Go 从 0 到精通 · 第 11 课:字符串、byte 与 rune
Go 从 0 到精通 · 第 11 课:字符串、byte 与 rune
学习定位:这是整套 Go 教程的第 11 课。
前置要求:已经完成第 10 课,掌握了 map 的增删改查、遍历和判断键是否存在。
本课目标:理解字符串的底层表示,掌握byte和rune的区别,能正确处理中文和多字节字符,理解字符串不可变性。
1. 本课你要解决的核心问题
你已经用过字符串很多次了,但可能遇到过这样的困惑:
len("你好")返回6而不是2,为什么?- 用
s[0]取第一个"字符",拿到的不是"你",而是一个奇怪的数字,为什么? for range遍历中文字符串时下标不连续(第 5 课提到过),为什么?
答案藏在 Go 字符串的底层编码方式里。
你需要搞明白以下问题:
- Go 的字符串在内存中到底是什么
byte和rune分别是什么len()算的是字节数还是字符数- 怎么正确遍历中文字符串
- 字符串为什么不可变
- 怎么做字符串和
[]byte、[]rune之间的转换
学完这一课,你就能正确处理包含中文、emoji 等多字节字符的场景了。
2. 字符串的底层真相
2.1 字符串是字节序列
Go 的字符串底层就是一个 []byte(字节切片)。它存储的是 UTF-8 编码的字节流。
1 | package main |
输出:
1 | 5 |
对于纯英文,每个字符恰好占 1 个字节,len 返回的字节数等于字符数。
2.2 中文字符占多个字节
1 | package main |
输出:
1 | 6 |
"你"在 UTF-8 中占 3 个字节(228, 189, 160),"好"也占 3 个字节(229, 165, 187)。
所以 len("你好") 返回 6(6 个字节),而不是 2(2 个字符)。
这就是第 5 课 for range 遍历中文下标不连续的原因——下标是字节位置,不是字符位置。
3. byte 与 rune
3.1 byte = uint8
byte 是 uint8 的别名,表示一个字节(8 位)。
1 | var b byte = 65 // 'A' |
用 s[i] 访问字符串得到的就是 byte——一个字节。对于中文,一个 byte 不足以表示一个完整字符。
3.2 rune = int32
rune 是 int32 的别名,表示一个 Unicode 码点(一个 Unicode 字符)。
1 | var r rune = '你' |
- 一个
rune能表示任意 Unicode 字符,包括中文、emoji rune占 4 个字节(32 位)
3.3 对比
| 类型 | 别名 | 含义 | 大小 |
|---|---|---|---|
byte |
uint8 |
一个字节 | 1 字节 |
rune |
int32 |
一个 Unicode 码点 | 4 字节 |
简单记忆:
byte:看字节rune:看字符
4. 字符串的两种遍历方式
4.1 按字节遍历(for i)
1 | s := "你好Go" |
len(s) 返回字节数(7),s[i] 返回 byte。中文的单个字节没有完整含义。
4.2 按字符遍历(for range)
1 | s := "你好Go" |
输出:
1 | 下标 0: 你 |
for range 遍历字符串时,按 rune(Unicode 码点)遍历:
i是当前字符起始的字节下标r是当前字符的rune值
4.3 怎么选
- 需要处理每个字符(包括中文)→ 用
for range - 需要处理每个字节(比如处理二进制数据)→ 用
for i
绝大多数文本处理场景都应该用 for range。
5. 字符串不可变
5.1 字符串一旦创建就不能修改
1 | s := "Hello" |
Go 的字符串是不可变的(immutable)。创建之后,内容不能被修改。
5.2 需要修改时怎么办
先把字符串转成 []byte 或 []rune,修改后再转回 string:
1 | package main |
对于中文,用 []rune:
1 | package main |
5.3 不可变性的好处
- 并发安全:多个 goroutine 可以安全地读取同一个字符串,不需要锁
- 可以安全地共享底层字节(切片截取不会互相影响)
- 字符串可以作为 map 的键(因为不可变,哈希值稳定)
6. 常用的字符串操作
6.1 字符串长度
1 | s := "你好Go" |
需要"字符数"时,转成 []rune 再取 len。
6.2 字符串拼接
1 | a := "Hello" |
+ 拼接字符串简单直接,但大量拼接时效率低(每次拼接都创建新字符串)。大量拼接推荐用 strings.Builder(第 21 课会讲)。
6.3 字符串和 []byte 互转
1 | s := "Hello" |
转换会复制数据,不是共享底层内存。
6.4 字符串和 []rune 互转
1 | s := "你好" |
6.5 字符串比较
1 | a := "Hello" |
字符串比较按字节逐个比较,区分大小写。
7. strings 包速览
strings 包提供了大量字符串操作函数。这里先列举几个最常用的,第 21 课会系统讲解。
1 | import "strings" |
8. 一段综合示例
1 | package main |
9. 常见坑总结
9.1 用 len 取"字符串长度"
1 | s := "你好" |
len(s) 返回的是字节数,不是字符数。
需要字符数:len([]rune(s))。
9.2 用 s[i] 取中文字符
1 | s := "你好" |
s[i] 返回的是 byte,不是完整字符。对于中文,需要用 for range 或 []rune。
9.3 以为字符串可以修改
1 | s := "Hello" |
字符串不可变。需要修改就转成 []byte 或 []rune。
9.4 字符串截取按字节截
1 | s := "你好" |
对中文做字符串截取时,按字节截取可能导致字符被截断,产生乱码。
安全做法:先转 []rune,截取后转回 string:
1 | runes := []rune("你好世界") |
9.5 字符串比较区分大小写
1 | fmt.Println("Hello" == "hello") // false |
需要不区分大小写比较时,统一转小写或大写后再比:
1 | strings.ToLower("Hello") == strings.ToLower("hello") // true |
10. 本课练习
练习 1:计算字符数
要求:
- 定义字符串
s := "Hello你好World世界" - 分别打印字节数和字符数
- 解释为什么两个数字不同
练习 2:正确遍历中文
要求:
- 定义包含中文和英文的字符串
- 用
for range遍历,打印每个字符和它的字节下标 - 观察下标的变化规律
练习 3:安全截取子串
要求:
- 实现函数
substr(s string, start, length int) string - 按字符数(不是字节数)截取子串
- 例如
substr("你好世界", 1, 2)返回"好世"
练习 4:统计字符类型
要求:
- 给定一个字符串,统计其中中文字符数、英文字母数、数字数、空格数
- 例如
"Hello你好 123"→ 中文 2、字母 5、数字 3、空格 1
练习 5:字符串反转
要求:
- 实现函数
reverse(s string) string - 正确处理中文字符(不能按字节反转)
reverse("你好世界")返回"界世好你"
练习 6:判断回文
要求:
- 实现函数
isPalindrome(s string) bool - 正确处理中文,例如
"上海自来水来自海上"返回true
11. 自测题
11.1 概念题
- Go 的字符串底层是什么?
byte和rune分别是什么?len("你好")返回什么?为什么?for range遍历字符串时,i和r分别是什么?- 字符串是可变的还是不可变的?
- 怎么修改字符串中的某个字符?
s[0]返回什么类型?对中文字符串有意义吗?- 怎么安全地截取包含中文的子串?
- 字符串和
[]byte互转会复制数据吗? - 为什么字符串不可变是一个好设计?
12. 本课总结
这一课你学到了 Go 字符串的底层真相。
你现在应该已经理解:
- 字符串底层是
[]byte(UTF-8 编码的字节流) byte=uint8(一个字节),rune=int32(一个 Unicode 码点)len(s)返回字节数,不是字符数- 用
for range遍历字符串按rune遍历,可以正确处理中文 - 字符串不可变——修改需要转
[]byte或[]rune - 对中文做截取时,用
[]rune避免截断乱码 - 不可变性带来了并发安全和哈希稳定性等好处
13. 下一课预告
下一课我们学习 Go 中最重要的自定义类型能力:结构体 struct。
会重点讲:
- 结构体是什么,为什么需要它
- 结构体的定义、初始化、字段访问
- 结构体嵌套(组合)
- 结构体的值语义
学完下一课,你就能用结构体来建模现实中的业务对象了。





