Go 字符串是一个不可变的字节序列,通常是 UTF-8 文本。运算符直接作用于字符串本身(+、==、<、索引、切片),其他功能都在标准库的 strings 包里。
分割字符串
| 函数 | 按什么分割 | 说明 |
|---|---|---|
Split(s, sep) | 每个 sep | 保留空的部分;Split("", ",") 返回 [""],即一个空字符串 |
SplitN(s, sep, n) | 最多 n 个部分 | 最后一部分包含剩下的全部内容 |
SplitAfter(s, sep) | 每个 sep | 每一部分都保留分隔符 |
Fields(s) | 连续的空白 | 从不返回空的部分 |
FieldsFunc(s, f) | 使 f 为真的字符 | 自定义分隔符 |
Cut(s, sep) | 只在第一个 sep 处 | 返回前半部分、后半部分和是否找到(Go 1.18) |
Split("", ",") 这种情况经常让人踩坑:空输入得到的是长度为 1 的切片,而不是 0。分割用户输入时,用 Fields,或者先检查 s == "",就能避免多出一个空元素。
要分割 key=value、user@host 这类成对的值,strings.Cut 是最简洁的办法。Go 1.24 还加入了迭代器版本 strings.SplitSeq 和 strings.FieldsSeq,它们逐个产出各部分,不需要构建切片。
查找
| 函数 | 返回 |
|---|---|
Contains(s, sub) | sub 出现在 s 中时返回 true |
ContainsAny(s, chars) | chars 中任意一个字符出现时返回 true |
HasPrefix(s, p) / HasSuffix(s, p) | s 以 p 开头或结尾时返回 true |
Index(s, sub) | 第一个匹配的字节位置,找不到时返回 -1 |
LastIndex(s, sub) | 最后一个匹配的字节位置,找不到时返回 -1 |
Count(s, sub) | 不重叠匹配的个数 |
EqualFold(a, b) | 不区分大小写的相等比较 |
除了 EqualFold,所有查找都区分大小写。要不区分大小写地查找,先把两边都转成小写,或者在比较整个字符串时用 EqualFold。
转换
有一个陷阱:Trim、TrimLeft 和 TrimRight 接收的是一个字符集合,不是前缀。strings.TrimLeft("abcab", "ab") 会去掉开头所有的 a 和 b,返回 "cab"。要去掉确切的前缀或后缀,用 TrimPrefix 和 TrimSuffix。
拼接与 strings.Builder
+ 拼接字符串,fmt.Sprintf 把多个值格式化成一个字符串。因为字符串不可变,两者每次都会分配一个新字符串。操作次数少时没问题,在循环里就会变慢,因为每次 += 都要复制之前拼好的全部内容。这时改用 strings.Builder:
零值的 strings.Builder 可以直接使用。它实现了 io.Writer,所以 fmt.Fprintf 可以直接写入它。如果大致知道结果有多大,先调用 b.Grow(n)。写入之后不要复制 Builder;请传指针。
多行字符串与原始字符串
Go 有两种字符串字面量:
双引号字符串会处理转义序列(\n、\t、\"、\\、\u00e9),而且必须写在一行内。反引号括起来的原始字符串按字面意思保留所有内容,包括换行,所以多行字符串、正则表达式、JSON 和 Windows 路径都用它来写。原始字符串里不能包含反引号,其中的回车符会被丢弃。注意上面的查询以一个换行开头,因为字面量从开头的反引号之后立刻开始。
长度、索引与子串
len(s) 是字节数。s[i] 是一个字节,s[i:j] 是按字节位置截取的子串:
ï 和 \u00e9 各占两个字节,所以这个字符串有 12 个字节、10 个字符。只要位置来自 strings.Index 之类的函数,按字节截取就是安全的,因为它们返回的字节偏移量都落在字符边界上。要取“前 n 个字符”,先转换成 []rune。rune 和字节一节会详细讲解 UTF-8。
字符串不可变
你不能原地修改某个字节:s[0] = 'H' 会编译出错(cannot assign to s[0] (neither addressable nor a map index expression))。strings 中的每个函数都返回一个新字符串。要修改内容,先转换成 []byte 或 []rune,修改切片,再转换回来:
b := []byte("hello")
b[0] = 'H'
s := string(b) // "Hello"
不可变也是子串开销很小的原因:s[2:5] 与 s 共享内存,而不是复制一份。反过来,一个巨大字符串的小子串会让整个巨大字符串一直留在内存里。如果要保留大输入中的一小段,用 strings.Clone(Go 1.20)。
比较字符串
== 和 != 比较内容,<、> 逐字节比较,对 ASCII 来说就是字母顺序。strings.Compare 也存在,但很少需要;对 []string 调用 slices.Sort 用的就是 <。要按地区规则给非英文文本排序,用 golang.org/x/text/collate。
转换其他类型
数字通过 strconv.Itoa、strconv.FormatFloat 或 fmt.Sprintf 转成字符串,字符串通过 strconv.Atoi 和 strconv.ParseFloat 转成数字。不要写 string(42):它得到的是码点为 42 的字符 "*"。类型转换一节有完整的对照表。
常见问题
Go 中如何分割字符串?
用 strings.Split(s, sep):strings.Split("a,b,c", ",") 返回 ["a" "b" "c"]。要按任意长度的空白分割并去掉空的部分,用 strings.Fields(s)。strings.SplitN 限制分割出的部分数量,strings.Cut 只在第一个分隔符处切一次。
Go 中如何写多行字符串?
用反引号括起来的原始字符串字面量:
query := `SELECT id, name
FROM users
WHERE active = true`
反引号之间的所有内容都按字面意思保留,包括换行和反斜杠。原始字符串里不能包含反引号。
Go 中如何判断字符串是否包含子串?
strings.Contains(s, substr) 返回一个 bool。相关函数:strings.HasPrefix、strings.HasSuffix、strings.Index(返回位置,找不到时返回 -1)、strings.ContainsAny(是否包含一组字符中的任意一个),以及不区分大小写比较相等的 strings.EqualFold。
Go 中拼接字符串最快的方法是什么?
只拼几个字符串时,+ 就够了。在循环里构建字符串时,用 strings.Builder:反复调用 WriteString,最后调用一次 String()。在循环里反复 += 每次都会复制整个字符串,字符串越长就越慢。要用分隔符连接一个切片,用 strings.Join。
为什么 len 返回的字符串长度不对?
len(s) 返回的是字节数,不是字符数。Go 字符串使用 UTF-8 编码,ASCII 以外的字符占 2 到 4 个字节,所以 len("héllo") 是 6。要得到 Unicode 码点的数量,用 utf8.RuneCountInString(s)。