Go语言中的字符串本质是只读的字节切片,底层采用UTF-8编码存储字符。当字符串中包含中文、特殊符号、emoji等多字节字符时,直接按字节下标遍历字符串会出现字符截断、乱码的情况,这时候就需要按符文(Rune)来遍历字符串,才能正确获取每个完整的Unicode字符。

为什么不能直接按字节遍历字符串
UTF-8编码中,一个ASCII字符占1个字节,而中文、emoji等字符会占用2到4个字节不等。如果直接用for循环加下标遍历字符串,每次获取的是一个字节,无法拼合出完整的多字节字符。我们可以通过下面的示例看到差异:
package main
import (
"fmt"
)
func main() {
str := "你好Go"
// 按字节遍历
fmt.Println("按字节遍历结果:")
for i := 0; i < len(str); i++ {
fmt.Printf("%c ", str[i])
}
fmt.Println()
}
这段代码运行后输出的结果是ä ½ å ¥ ½ G o,每个中文字符被拆分成了多个无意义的字节,显然不符合我们的预期。
按符文遍历的两种常用方式
方式一:使用for range循环遍历
Go语言的for range循环遍历字符串时,默认会按符文遍历,每次迭代返回的是符文的索引和对应的rune值,这种方式是最简洁的按符文遍历方法:
package main
import (
"fmt"
)
func main() {
str := "你好Go"
// for range按符文遍历
fmt.Println("for range遍历结果:")
for index, r := range str {
fmt.Printf("索引:%d, 符文:%c, Unicode码点:%Un", index, r, r)
}
}
运行这段代码可以看到,每次迭代都能正确获取到完整的字符,中文字符和英文字符都被正确处理,索引也会自动跳过多字节字符的所有字节位置。
方式二:将字符串转换为rune切片遍历
如果需要随机访问符文,或者需要多次操作符文序列,可以先将字符串转换为[]rune类型,再遍历这个切片。转换后每个元素都是一个完整的rune,长度就是符文的个数:
package main
import (
"fmt"
)
func main() {
str := "你好Go"
// 转换为rune切片
runeSlice := []rune(str)
fmt.Println("rune切片遍历结果:")
for i, r := range runeSlice {
fmt.Printf("索引:%d, 符文:%cn", i, r)
}
// 可以直接获取符文个数
fmt.Printf("字符串符文个数:%dn", len(runeSlice))
}
这种方式的优势是可以像操作普通切片一样操作符文序列,比如修改某个位置的符文、截取部分符文序列等,不过转换过程会创建一个新的切片,会占用额外的内存。
两种方式的适用场景对比
我们可以通过下面的表格对比两种按符文遍历方式的特点,方便根据实际需求选择:
| 遍历方式 | 特点 | 适用场景 |
|---|---|---|
| for range遍历 | 无需额外转换,内存占用小,自动处理UTF-8解码 | 只需要顺序遍历字符串,不需要随机访问符文 |
| 转换为[]rune遍历 | 支持随机访问,可修改符文序列,能直接获取符文个数 | 需要多次操作符文、随机访问符文、获取符文总数 |
注意事项
- for range遍历字符串时,返回的索引是当前符文第一个字节在原始字符串中的位置,不是符文的序号,如果需要符文序号需要自己维护计数器。
- 转换为
[]rune后,切片的长度是符文的个数,和原始字符串的len()返回的字节数不同,不要混淆这两个长度。 - 如果字符串中包含无效的UTF-8字节序列,for range遍历会将其替换为Unicode替换字符uFFFD,而转换为[]rune也会做同样的处理,不会出现 panic。
在实际开发中,只要字符串可能包含非ASCII字符,就应该优先选择按符文遍历的方式,避免字节遍历带来的乱码问题。根据是否需要随机访问符文的需求,选择for range或者转换为rune切片的方式即可。