字符串与 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 / HasSuffix | HasPrefix(str, "He") → true;HasPrefix(str, "he") → false(区分大小写)15 |
| 判包含 | Contains / ContainsAny | Contains(str, "o G") → true;ContainsAny(str, "welcome") → true(任一字符命中)16 |
| 数次数 | Count | Count("Grow And Grow With Golang", "Gr") → 217 |
| 找位置 | Index / LastIndex | Index(str, "Gr") → 0;找不到返回 -118 |
| 按字符集找 | IndexAny / LastIndexAny | 在字符集里任一字第一次/最后一次出现的位置19 |
| 拼接 | Join | Join(["my","name","is","rahul"], "-") → my-name-is-rahul20 |
| 替换 | Replace / ReplaceAll | 第四个参数是最多替换几处:0 = 不动,1 = 只换第一处,给大数 = 全换21 |
| 切分 | Split / Fields | Split("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. 可带走的
- Go 字符串 = 字节串,UTF-8 编码;双引号定义,单引号是字符(rune);
len数的是字节数:"世Hello Go世"是 14 不是 8;- 按下标遍历按字节走,遇到中文就切碎;正路是
[]rune(s)或range; - range 遍历字符串按码点迈步,item 是 rune;
- rune = int32 = 码点;字符能比较大小(
'a' > 'A')、能递增; - 字符串不可变;修改 = 转 rune 切片 → 改 → 转回新字符串;
- 反引号字符串:转义无效、真能换行,适合多行文本和正则;
- strings 包高频五件套:
Contains、Index(-1 表示没有)、Replace(第 4 个参数限次数)、Split、Trim家族; HasPrefix/Contains都区分大小写;- 替换全部用
ReplaceAll,切分后拼回用Join,按空白切用Fields。
10. 原文地图
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 字符串=字节切片、双引号规矩 | Strings | text/17-fm-learning-go-programming.txt:58(搜「slice of bytes」) |
| UTF-8 与码点 | Strings | text/17-fm-learning-go-programming.txt:61(搜「code points」) |
| len 数字节(14 vs 8) | Strings | text/17-fm-learning-go-programming.txt:127(搜「total number of bytes」) · text/17-fm-learning-go-programming.txt:163(搜「length is 14」) |
| 按字节遍历出乱码及分析 | Strings | text/17-fm-learning-go-programming.txt:250(搜「ä ¸」) · text/17-fm-learning-go-programming.txt:266(搜「things can go wrong」) |
| rune 类型与 []rune 遍历 | Strings | text/17-fm-learning-go-programming.txt:272(搜「alias of int32」) · text/17-fm-learning-go-programming.txt:287(搜「rune_slice := []rune」) |
| range 按码点遍历 | Strings | text/17-fm-learning-go-programming.txt:335(搜「covers each byte」) · text/17-fm-learning-go-programming.txt:362(搜「for _, item := range str_new」) |
| 反引号字符串 | Strings | text/17-fm-learning-go-programming.txt:386(搜「backtick」) · text/17-fm-learning-go-programming.txt:419(搜「treats \n as normal character」) |
| rune/byte 切片转字符串 | Strings | text/17-fm-learning-go-programming.txt:479(搜「世HelloGo世」) |
| 不可变与「改字符」三步舞 | Strings | 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」) |
| 字符的比较与递增 | Strings | text/17-fm-learning-go-programming.txt:593(搜「a is greater tha A」) |
| strings 包总述 | Strings | text/17-fm-learning-go-programming.txt:665(搜「UTF-8 encoded strings」) |
| ToUpper/ToLower | Strings | text/17-fm-learning-go-programming.txt:704(搜「HELLO GO」) |
| HasPrefix/HasSuffix | Strings | text/17-fm-learning-go-programming.txt:716(搜「starts with a specified string」) |
| Contains/ContainsAny | Strings | text/17-fm-learning-go-programming.txt:755(搜「Checks if a string exists」) |
| Count | Strings | text/17-fm-learning-go-programming.txt:791(搜「number of times」) · text/17-fm-learning-go-programming.txt:809(搜「occurences of Gr: 2」) |
| Index/LastIndex 与 -1 | Strings | text/17-fm-learning-go-programming.txt:821(搜「first occurrence」) · text/17-fm-learning-go-programming.txt:899(搜「last position of Gr: 9」) |
| Join | Strings | text/17-fm-learning-go-programming.txt:953(搜「my-name-is-rahul」) |
| Replace 的第 4 个参数 | Strings | text/17-fm-learning-go-programming.txt:959(搜「number of occurrences」) · text/17-fm-learning-go-programming.txt:980(搜「my name-is-rahul」) |
| Split/Fields | Strings | text/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 家族 | Strings | text/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
-
出处:「Strings」第 58 段(
text/17-fm-learning-go-programming.txt:58,搜「slice of bytes」)。 ↩ ↩2 -
出处:「Strings」第 61 段(
text/17-fm-learning-go-programming.txt:61,搜「code points」)。 ↩ -
出处:「Strings」第 67–118 段(
text/17-fm-learning-go-programming.txt:100,搜「By default strings are」)。 ↩ -
出处:「Strings」第 127–163 段(
text/17-fm-learning-go-programming.txt:145,搜「世Hello Go世」;text/17-fm-learning-go-programming.txt:163,搜「length is 14」)。 ↩ -
出处:「Strings」第 127 段(
text/17-fm-learning-go-programming.txt:127,搜「total number of bytes」)。 ↩ -
出处:「Strings」第 250–266 段(
text/17-fm-learning-go-programming.txt:250,搜「ä ¸」;text/17-fm-learning-go-programming.txt:266,搜「things can go wrong」)。 ↩ ↩2 -
出处:「Strings」第 269–329 段(
text/17-fm-learning-go-programming.txt:272,搜「alias of int32」)。 ↩ -
出处:「Strings」第 332–380 段(
text/17-fm-learning-go-programming.txt:335,搜「covers each byte」)。 ↩ -
出处:「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」)。 ↩ -
出处:「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)。 ↩ -
出处:「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 -
出处:「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 -
出处:「Strings」第 662–668 段(
text/17-fm-learning-go-programming.txt:665,搜「UTF-8 encoded strings」)。 ↩ -
出处:「Strings」第 677–710 段(
text/17-fm-learning-go-programming.txt:704,搜「HELLO GO」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Strings」第 755–785 段(
text/17-fm-learning-go-programming.txt:767,搜「contains ellO」)。注意:书中 ContainsAny(str, "welcome") 标为 true,但 "Hello Go" 不含 welcome 中任何字符,应为 false,疑为笔误。 ↩ ↩2 -
出处:「Strings」第 791–815 段(
text/17-fm-learning-go-programming.txt:809,搜「occurences of Gr: 2」)。 ↩ -
出处:「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」)。 ↩ -
出处:「Strings」第 851–932 段(
text/17-fm-learning-go-programming.txt:869,搜「Unicode codepoint in Ground: 0」)。 ↩ -
出处:「Strings」第 938–956 段(
text/17-fm-learning-go-programming.txt:953,搜「my-name-is-rahul」)。 ↩ -
出处:「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 -
出处:「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」)。 ↩ -
出处:「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