go语言:string字符串

介绍

Go 的字符串(string)是原生类型,底层是只读字节切片,默认采用 UTF-8 编码。

注意:字符串不可修改,存储的是字节序列,不是字符。

1. 定义

var s string = "hello"
s2 := `这是原始字符串,可以换行,不转义\n`

两种字面量:

  1. 双引号 " ":解析转义字符 \n \t \",不能直接换行
  2. 反引号 ` :原始字符串,原样输出,不转义,支持多行

底层结构(runtime/string.go)

type string struct {
    ptr unsafe.Pointer // 指向字节数组的指针
    len int            // 字节长度
}
  • string 本身是一个结构体值,赋值时拷贝 ptr 和 len,底层字节数组共享
  • 底层字节数组只读:不能 s[0] = 'a',编译报错

2. 字节 vs 字符

Go string 存字节。

  • 英文字母:UTF-8 占 1 字节
  • 中文汉字:UTF-8 占 3 字节
s := "Go语言"
fmt.Println(len(s)) // 输出 5:G(1)+o(1)+语(3)+言(3) = 8

len(s) 返回字节长度,不是字符个数。

rune字符

rune:Go 的字符类型,int32 别名,代表一个 Unicode 码点。
把字符串转 rune 切片获取字符数量:

r := []rune(s)
fmt.Println(len(r)) // 4,一共4个字符

遍历字符串两种方式

  1. 普通 for 循环(按字节索引)

    s := "语言"
    for i := 0; i < len(s); i++ {
     fmt.Printf("%d: %c\n", i, s[i]) // s[i] 是 byte,字节
    }
    

    中文会输出乱码,因为索引定位到字节,不是完整字符。

  2. for range(按 Unicode 码点遍历,推荐)

    s := "语言"
    for idx, r := range s {
     fmt.Printf("字符位置%d, rune:%c\n", idx, r)
    }
    

    range 返回的 idx 是字节偏移量,不是字符序号。

3. 字符串不可变性

s := "abc"
s[0] = 'x' // ❌ 编译错误:string is immutable

想要修改字符串:转成 []byte 修改,再转回 string

s := "abc"
b := []byte(s)
b[0] = 'x'
s = string(b)

注意:转换会拷贝底层数据,产生新内存。

4. 常用操作

4.1 拼接

  1. + 最简单,少量拼接用;每次 + 生成新字符串,大量拼接性能差
    a := "hello" + " " + "world"
    
  2. strings.Builder ✅ 大量拼接首选,内部维护字节切片,减少内存拷贝

    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteString(" world")
    res := sb.String()
    

    重要:strings.Builder 不要拷贝,一旦拷贝底层共享数组会 panic。

  3. fmt.Sprintf:格式化拼接,性能一般,适合带格式化场景

    s := fmt.Sprintf("name=%s, age=%d", "张三", 20)
    
  4. strings.Join:字符串切片拼接

    arr := []string{"a", "b", "c"}
    res := strings.Join(arr, ",") // "a,b,c"
    

4.2 字符串包 strings(标准库)

import "strings"

常用函数:

  • strings.Contains(s, substr string) bool 是否包含子串
  • strings.HasPrefix(s, prefix string) bool 前缀
  • strings.HasSuffix(s, suffix string) bool 后缀
  • strings.Index(s, substr string) int 返回子串起始字节索引,找不到返回-1
  • strings.LastIndex 最后出现位置
  • strings.Split(s, sep string) []string 分割
  • strings.ReplaceAll(s, old, new string) string 全部替换
  • strings.ToUpper / ToLower 大小写转换
  • strings.TrimSpace(s string) 去除首尾空白(空格、换行、tab)
  • strings.Trim(s, cutset string) 去除首尾指定字符集

4.3 类型转换

  1. string ↔ []byte
    s := "test"
    b := []byte(s) // string -> []byte,拷贝字节
    s2 := string(b) // []byte -> string,拷贝字节
    
  2. string ↔ []rune
    r := []rune("中文")
    s := string(r)
    

4.4 子串切片

字符串切片 s[low:high],切的是字节区间,不是字符!

s := "Go语言"
sub := s[0:2] // "Go"
sub2 := s[2:5] // "语"(3字节)

⚠️ 如果切在中文UTF-8字节中间,得到的子串是无效UTF-8,会出现乱码。

5. 内存与性能关键点

  1. 字符串比较 ==:直接比较底层字节,高效;区分大小写
  2. 字符串切片 s[a:b]:不拷贝底层字节,只创建新 string 结构体(ptr+len),共享原字节数组。

    坑:如果原字符串很大,切一个很短子串保留引用,大内存不会被GC释放。

示例:

big := strings.Repeat("a", 1024*1024) // 1MB字符串
small := big[:5] // small 共享big底层数组,big内存无法GC

解决:需要独立内存时,转 []byte 再转回 string,强制拷贝:

small := string([]byte(big[:5]))
  1. 空字符串:"",底层 ptr=nil, len=0,占用极少内存
  2. 字符串常量:编译期确定,放在只读内存段

6. UTF-8 相关标准库

unicode / unicode/utf8

import "unicode/utf8"
utf8.RuneCountInString(s) // 获取字符数量(等价 len([]rune(s)),但不创建切片,性能更好)
utf8.DecodeRuneInString(s) // 读取第一个rune和字节长度

7. 常见坑总结

  1. len(s) 是字节数,不是汉字/字符数
  2. s[i] 取的是 byte,不是 rune;中文不能用索引直接取字符
  3. s[low:high] 按字节切片,截断在多字节字符中间 → 乱码
  4. string 不可变,s+ 大量拼接会频繁分配内存,优先 strings.Builder
  5. 字符串切片共享底层数组,长字符串截取短子串会内存泄漏
  6. range 遍历得到的 index 是字节偏移,不是字符序号

8. 例子

package main

import (
    "fmt"
    "strings"
    "unicode/utf8"
)

func main() {
    s := "Go语言"
    fmt.Println("字节长度:", len(s))
    fmt.Println("字符数量:", utf8.RuneCountInString(s))

    // range遍历
    for offset, r := range s {
        fmt.Printf("字节偏移:%d, 字符:%c\n", offset, r)
    }

    // 拼接
    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteString(" 世界")
    fmt.Println(sb.String())

    // 判断包含
    fmt.Println(strings.Contains(s, "语言"))
}

如果你需要,我可以单独整理一份 Go字符串面试题清单 或者 strings包完整API速查表。