编程语言研究书库

字符串与 rune:字节的世界里,字符怎么数

字符串与 rune:字节的世界里,字符怎么数

这一章回答一个 Go 新手必撞的问题: len("世Hello Go世") 为什么是 14 不是 8? 答案会牵出 Go 字符串的全部真相:它是字节串,不是字符串;中文一个字符占 3 个字节。 读完你会知道什么时候按字节干活、什么时候必须转成 rune、 以及 strings 包里日常最常用的那些函数。

1. 先把定义钉死

书里给的原话值得记:别的语言把字符串定义成「字符的数组」, Go 把它定义成「字节的切片」1。

围绕这句话,有三条配套规矩:

  • 字符串永远用双引号 "..." 定义;单引号只用来定义单个字符(那是 rune,不是字符串)1;
  • 默认 UTF-8 编码:每个字符被翻译成 1 到 4 个字节, 具体几个字节取决于这个字符在 Unicode 表里的位置;
  • Unicode 码点(code point):给全世界每个字符编的唯一编号,共约 111 万个位置2。

零值照例是空串 ""——书里演示过声明不赋值时 %q 打印出来是 ""3。

2. 主线例子:「世Hello Go世」的三次体检

这是本章主线,照原书的例子走完4:

第一次体检:len。

str := "Hello Go"
len(str)          // → 8        8 个英文字母,8 个字节,没人觉得不对

str_new := "世Hello Go世"
len(str_new)      // → 14 ?!   明明是 8 个字符

14 = 3 + 8 + 3:「世」在 UTF-8 里占 3 个字节(e4 b8 96), 两边的两个「世」各 3 字节,加中间 8 个英文字母,共 14。 书里原话:len 返回的是每个字符占的字节数的总和5。

第二次体检:按下标遍历。

for i := 0; i < len(str_new); i++ {
    fmt.Printf("%c ", str_new[i])
}
// → ä ¸ – H e l l o   G o ä ¸ –

乱码了。「世」的 3 个字节被当成 3 个独立字符,各打印一次, 自然不是「世」本人。书里对故障的分析很清楚: 遍历按字节走,一个汉字跨了 3 个字节,把它当 1 个字符打印,必然出错6。

第三次体检:两条正路。

正路一:转成 rune 切片再遍历。

rune_slice := []rune(str_new)   // 按码点切,一个字符一项
// → 世 H e l l o   G o 世     ← 正确

rune 是 int32 的别名,装的是码点,和字符占几个字节完全解耦7。

正路二:用 range 遍历字符串。

for _, item := range str_new {
    fmt.Printf("%c ", item)
}
// → 世 H e l l o   G o 世     ← 同样正确

range 遍历字符串时,按码点而不是按字节迈步——每轮的 item 是 rune。 这是日常写 Go 处理文本的默认姿势8。

判断(我们的,不是书里的): 「字节 vs 字符」这道坎,是所有 C 系语言 都会让程序员摔一次的坑,Go 没替你抹平,只是给了两条体面的路(rune、range)。 实际写码时的口诀:要长度、要切片,想明白你要的是字节还是字符; 要遍历、要处理文本,用 range。 如果错,会错在: 纯 ASCII 场景(日志、协议头、URL)按字节处理又快又对, 「必须 range」会误导人——按不按字节,取决于数据里有没有多字节字符。

3. rune 的双向转换与「字符就是数」

rune 切片和字符串互相转,都是一行:

s := string([]rune{0x4e16, 0x48, 0x65, 0x6c, 0x6c, 0x6f, 0x47, 0x6f, 0x4e16})
// → 世HelloGo世

s := string([]byte{228, 184, 150, 72, 101, ...})
// → 世HelloGo世   ← 字节切片也能转回字符串

两个方向都演示过9。rune 是 int32,所以字符能比较大小、能算术: 'a' > 'A' 为 true(97 > 65),for c := 'a'; c < 'm'; c++ 能从 a 数到 l, 输出形如 97 a、98 b……10。这个「字符就是数」的直觉,在处理 字母表、字符范围时很好用。

4. 不可变:想改字符串,先变成 rune

书里把规矩说死了:字符串一旦定义就不能改,它是只读的字节串。 想改某个字符或加长,编译器直接报错11:

str := "Hello Go"
fmt.Printf("%c", str[3])   // → l   读,可以
str[3] = 'y'               // 编译错:cannot assign to str[3]

正路是「转出去、改、转回来」的三步舞:

r := []rune(str)   // 1. 字符串 → rune 切片(这是新造的数据,随便改)
r[3] = 'M'         // 2. 改
str = string(r)    // 3. rune 切片 → 新字符串
// str[3] 现在是 M

书里演示的结果:l 变成 M11。 注意这个过程中原字符串一个字节都没动过——它只是不再被引用, 垃圾回收会来收走。「不可变 + 新建」是 Go 字符串的全部修改方式, 字符串拼接(+)背后也是同一个原理。

5. 反引号:原始字符串,换行和转义都不特殊

双引号字符串里,\n 是换行、\" 是引号——反引号字符串里,什么都没有特殊含义: 字面上是什么就是什么,真的换行就是换行,\n 两个字符就原样打印 \n12:

s := `Hello\n Go`     // 里面就是反斜杠+n,不是换行
s := `
Hello
Go
`                      // 换行就是换行,不用写 \n

多行文本、正则表达式、文件路径,反引号都好使。 书里特别提醒:双引号字符串不能真的换行(编译错),反引号可以12。

6. strings 包:日常函数速查

strings 是标准库里的字符串工具包,import 后用点号调它的导出函数13。 原书逐个演示,压缩成这张表(例子的宿主字符串大多是 "Hello Go" 和 "Grow And Grow With Golang",结果照原书):

想干什么函数例子与结果
转大/小写ToUpper / ToLower"Hello Go" → HELLO GO / hello go14
判前缀/后缀HasPrefix / HasSuffixHasPrefix(str, "He") → true;HasPrefix(str, "he") → false(区分大小写)15
判包含Contains / ContainsAnyContains(str, "o G") → true;ContainsAny(str, "welcome") → true(任一字符命中)16
数次数CountCount("Grow And Grow With Golang", "Gr") → 217
找位置Index / LastIndexIndex(str, "Gr") → 0;找不到返回 -118
按字符集找IndexAny / LastIndexAny在字符集里任一字第一次/最后一次出现的位置19
拼接JoinJoin(["my","name","is","rahul"], "-") → my-name-is-rahul20
替换Replace / ReplaceAll第四个参数是最多替换几处:0 = 不动,1 = 只换第一处,给大数 = 全换21
切分Split / FieldsSplit("my-name", "-") → [my name];Fields 按空白切,多个空格/tab/换行当一个22
去首尾Trim 家族Trim 去两端指定字符;TrimSpace 去两端空白;TrimPrefix/TrimSuffix 去前缀/后缀(去不掉就原样返回)23

Replace 的第四个参数是高频坑:书里演示 Replace(str, "-", " ", 0) 返回原串, 1 只换第一处,9 全换21。想全换,直接用 ReplaceAll。

Trim 家族的返回值规律也值得记住:TrimPrefix/TrimSuffix 去不掉就原样返回 (TrimSuffix("-my-name-is-rahul-", "welcome") → 原串不动)23。

7. 作者的判断与证据

这一章几乎全是机制演示。有一处值得点出的细心: 书里没有回避「按字节打印中文出乱码」这个失败输出,而是把 e4 b8 96 逐字节 摆出来讲清原因——这比只给正路更有教学价值6。

8. 边界与局限

  • strings 包只演示了最常用的十几个函数,Builder(高效拼字符串)、 NewReplacer 等未出现;高频拼接场景应优先 strings.Builder(补充,不在书里);
  • 字符串和数字互转(strconv 包)本章没讲,日常用到 strconv.Itoa、Atoi;
  • 书中 ContainsAny(str, "welcome") 的例子结果标为 true, 但 "welcome" 里的字符(w,e,l,c,o,m)没有一个出现在 "Hello Go" 中—— 按语义应为 false,疑为原书笔误16(函数机制本身以我们的描述为准)。

9. 可带走的

  1. Go 字符串 = 字节串,UTF-8 编码;双引号定义,单引号是字符(rune);
  2. len 数的是字节数:"世Hello Go世" 是 14 不是 8;
  3. 按下标遍历按字节走,遇到中文就切碎;正路是 []rune(s) 或 range;
  4. range 遍历字符串按码点迈步,item 是 rune;
  5. rune = int32 = 码点;字符能比较大小('a' > 'A')、能递增;
  6. 字符串不可变;修改 = 转 rune 切片 → 改 → 转回新字符串;
  7. 反引号字符串:转义无效、真能换行,适合多行文本和正则;
  8. strings 包高频五件套:Contains、Index(-1 表示没有)、Replace(第 4 个参数限次数)、Split、Trim 家族;
  9. HasPrefix/Contains 都区分大小写;
  10. 替换全部用 ReplaceAll,切分后拼回用 Join,按空白切用 Fields。

10. 原文地图

主题原书章原文位置
字符串=字节切片、双引号规矩Stringstext/17-fm-learning-go-programming.txt:58(搜「slice of bytes」)
UTF-8 与码点Stringstext/17-fm-learning-go-programming.txt:61(搜「code points」)
len 数字节(14 vs 8)Stringstext/17-fm-learning-go-programming.txt:127(搜「total number of bytes」) · text/17-fm-learning-go-programming.txt:163(搜「length is 14」)
按字节遍历出乱码及分析Stringstext/17-fm-learning-go-programming.txt:250(搜「ä ¸」) · text/17-fm-learning-go-programming.txt:266(搜「things can go wrong」)
rune 类型与 []rune 遍历Stringstext/17-fm-learning-go-programming.txt:272(搜「alias of int32」) · text/17-fm-learning-go-programming.txt:287(搜「rune_slice := []rune」)
range 按码点遍历Stringstext/17-fm-learning-go-programming.txt:335(搜「covers each byte」) · text/17-fm-learning-go-programming.txt:362(搜「for _, item := range str_new」)
反引号字符串Stringstext/17-fm-learning-go-programming.txt:386(搜「backtick」) · text/17-fm-learning-go-programming.txt:419(搜「treats \n as normal character」)
rune/byte 切片转字符串Stringstext/17-fm-learning-go-programming.txt:479(搜「世HelloGo世」)
不可变与「改字符」三步舞Stringstext/17-fm-learning-go-programming.txt:518(搜「read-only slice of bytes」) · text/17-fm-learning-go-programming.txt:533(搜「cannot assign」) · text/17-fm-learning-go-programming.txt:578(搜「after changing its value: M」)
字符的比较与递增Stringstext/17-fm-learning-go-programming.txt:593(搜「a is greater tha A」)
strings 包总述Stringstext/17-fm-learning-go-programming.txt:665(搜「UTF-8 encoded strings」)
ToUpper/ToLowerStringstext/17-fm-learning-go-programming.txt:704(搜「HELLO GO」)
HasPrefix/HasSuffixStringstext/17-fm-learning-go-programming.txt:716(搜「starts with a specified string」)
Contains/ContainsAnyStringstext/17-fm-learning-go-programming.txt:755(搜「Checks if a string exists」)
CountStringstext/17-fm-learning-go-programming.txt:791(搜「number of times」) · text/17-fm-learning-go-programming.txt:809(搜「occurences of Gr: 2」)
Index/LastIndex 与 -1Stringstext/17-fm-learning-go-programming.txt:821(搜「first occurrence」) · text/17-fm-learning-go-programming.txt:899(搜「last position of Gr: 9」)
JoinStringstext/17-fm-learning-go-programming.txt:953(搜「my-name-is-rahul」)
Replace 的第 4 个参数Stringstext/17-fm-learning-go-programming.txt:959(搜「number of occurrences」) · text/17-fm-learning-go-programming.txt:980(搜「my name-is-rahul」)
Split/FieldsStringstext/17-fm-learning-go-programming.txt:989(搜「array of substrings」) · text/17-fm-learning-go-programming.txt:1010(搜「welcome in the world of Go」)
Trim 家族Stringstext/17-fm-learning-go-programming.txt:1016(搜「leading and trailing」) · text/17-fm-learning-go-programming.txt:1064(搜「TrimPrefix」) · text/17-fm-learning-go-programming.txt:1076(搜「TrimSpace」)

Footnotes

  1. 出处:「Strings」第 58 段(text/17-fm-learning-go-programming.txt:58,搜「slice of bytes」)。 ↩ ↩2

  2. 出处:「Strings」第 61 段(text/17-fm-learning-go-programming.txt:61,搜「code points」)。 ↩

  3. 出处:「Strings」第 67–118 段(text/17-fm-learning-go-programming.txt:100,搜「By default strings are」)。 ↩

  4. 出处:「Strings」第 127–163 段(text/17-fm-learning-go-programming.txt:145,搜「世Hello Go世」;text/17-fm-learning-go-programming.txt:163,搜「length is 14」)。 ↩

  5. 出处:「Strings」第 127 段(text/17-fm-learning-go-programming.txt:127,搜「total number of bytes」)。 ↩

  6. 出处:「Strings」第 250–266 段(text/17-fm-learning-go-programming.txt:250,搜「ä ¸」;text/17-fm-learning-go-programming.txt:266,搜「things can go wrong」)。 ↩ ↩2

  7. 出处:「Strings」第 269–329 段(text/17-fm-learning-go-programming.txt:272,搜「alias of int32」)。 ↩

  8. 出处:「Strings」第 332–380 段(text/17-fm-learning-go-programming.txt:335,搜「covers each byte」)。 ↩

  9. 出处:「Strings」第 449–512 段(text/17-fm-learning-go-programming.txt:479,搜「世HelloGo世」;text/17-fm-learning-go-programming.txt:512,搜「String from a slice of bytes」)。 ↩

  10. 出处:「Strings」第 581–658 段(text/17-fm-learning-go-programming.txt:593,搜「a is greater tha A」;text/17-fm-learning-go-programming.txt:626,搜「97 a」)。原文即此拼写(tha)。 ↩

  11. 出处:「Strings」第 515–578 段(text/17-fm-learning-go-programming.txt:518,搜「read-only slice of bytes」;text/17-fm-learning-go-programming.txt:533,搜「cannot assign」;text/17-fm-learning-go-programming.txt:578,搜「after changing its value: M」)。 ↩ ↩2

  12. 出处:「Strings」第 383–446 段(text/17-fm-learning-go-programming.txt:386,搜「backtick」;text/17-fm-learning-go-programming.txt:419,搜「treats \n as normal character」)。 ↩ ↩2

  13. 出处:「Strings」第 662–668 段(text/17-fm-learning-go-programming.txt:665,搜「UTF-8 encoded strings」)。 ↩

  14. 出处:「Strings」第 677–710 段(text/17-fm-learning-go-programming.txt:704,搜「HELLO GO」)。 ↩

  15. 出处:「Strings」第 716–749 段(text/17-fm-learning-go-programming.txt:731,搜「has prefix He true」;text/17-fm-learning-go-programming.txt:737,搜「has prefix he false」)。 ↩

  16. 出处:「Strings」第 755–785 段(text/17-fm-learning-go-programming.txt:767,搜「contains ellO」)。注意:书中 ContainsAny(str, "welcome") 标为 true,但 "Hello Go" 不含 welcome 中任何字符,应为 false,疑为笔误。 ↩ ↩2

  17. 出处:「Strings」第 791–815 段(text/17-fm-learning-go-programming.txt:809,搜「occurences of Gr: 2」)。 ↩

  18. 出处:「Strings」第 821–908 段(text/17-fm-learning-go-programming.txt:845,搜「position of welcome: -1」;text/17-fm-learning-go-programming.txt:899,搜「last position of Gr: 9」)。 ↩

  19. 出处:「Strings」第 851–932 段(text/17-fm-learning-go-programming.txt:869,搜「Unicode codepoint in Ground: 0」)。 ↩

  20. 出处:「Strings」第 938–956 段(text/17-fm-learning-go-programming.txt:953,搜「my-name-is-rahul」)。 ↩

  21. 出处:「Strings」第 959–983 段(text/17-fm-learning-go-programming.txt:959,搜「number of occurrences」;text/17-fm-learning-go-programming.txt:980,搜「my name-is-rahul」)。 ↩ ↩2

  22. 出处:「Strings」第 989–1013 段(text/17-fm-learning-go-programming.txt:1007,搜「Split(str, "-")」;text/17-fm-learning-go-programming.txt:1010,搜「welcome in the world of Go」)。 ↩

  23. 出处:「Strings」第 1016–1076 段(text/17-fm-learning-go-programming.txt:1031,搜「Trim(str, "-")」;text/17-fm-learning-go-programming.txt:1064,搜「TrimPrefix」;text/17-fm-learning-go-programming.txt:1070,搜「Unchanged」)。 ↩ ↩2

On this page