Golang中不同for循环遍历string存在的小坑
众所周知,go语言中string是一个不可变的类型,由字节组成,字符串的内部结构由只读的字节数组和长度组成,结构如下:
type stringStruct struct {
str unsafe.Pointer // 指向底层字节数组
len int // 字符串长度(字节数)
}
可以"无缝地"与[]byte
进行转换,此时byte
数组中每个元素为字符串s
的一个字节。
s := "hello, world"
// string → []byte
b := []byte(s)
fmt.Println(b) // 输出字符的ASCII编码:[104 101 108 108 111 44 32 119 111 114 108 100]
// []byte → string
s2 := string(b)
每个字符在存储时采用的是UTF-8编码,而UTF-8是可变长度的编码方式,英文等ASCII字符占1个字节,而中文通常占3个字节。如字母A
是单字节,而你
在编码时占三个字节。由于string
存放的是字符,因此使用len()
时返回的是string
的字节数而不是字符数。
s := "Hello"
fmt.Println(len(s)) // 输出 5(字节数)
s := "你好,世界"
fmt.Println(len(s)) // 输出 15("你"(3)+"好"(3)+","(3)+"世"(3)+"界"(3))
而我们想要遍历字符串时,有多种方式进行遍历,如常规的索引循环
或者range循环
。此时如果面对纯ASCII
字符组成的字符串,两种遍历方式都能正确获取string
中的字符:
s := "Hello"
for i := 0; i < len(s); i++ {
fmt.Printf("字节索引 %d: %c\n", i, s[i])
}
// Output:
字节索引 0: H
字节索引 1: e
字节索引 2: l
字节索引 3: l
字节索引 4: o
for i, c := range s {
fmt.Printf("字节索引 %d: %c\n", i, c)
}
// Output:
字节索引 0: H
字节索引 1: e
字节索引 2: l
字节索引 3: l
字节索引 4: o
但是如果面对存在非ASCII字符组成的字符串时,两种for循环遍历,就会存在不同的结果:
// range循环能正常输出字符
s := "你好,世界"
for i, c := range s {
fmt.Printf("字节索引 %d: %c\n", i, c)
}
// Output:
字节索引 0: 你
字节索引 3: 好
字节索引 6: ,
字节索引 9: 世
字节索引 12: 界
// 下标遍历则存在问题
s := "你好,世界"
for i := 0; i < len(s); i++ {
fmt.Printf("字节索引 %d: %c\n", i, s[i])
}
// Output:
字节索引 0: ä
字节索引 1: ½
字节索引 2:
字节索引 3: å
字节索引 4: ¥
字节索引 5: ½
字节索引 6: ï
字节索引 7: ¼
字节索引 8: Œ
字节索引 9: ä
字节索引 10: ¸
字节索引 11: –
字节索引 12: ç
字节索引 13: •
字节索引 14:
究其原因,下标遍历时,每个下标索引的是字符串底层字符数组的每个字符位置,而range遍历时,是根据字符串编码的字符进行索引,我们通过输出每个元素的类型可以发现,s[i]
是uint8类型的元素,而c
是int32
(或rune
类型,rune底层为int32)类型的元素:
s := "你好,世界"
for i := 0; i < len(s); i++ {
fmt.Printf("%T\n", s[i])
break
}
for _, c := range s {
fmt.Printf("%T\n", c)
break
}
// Output:
uint8
int32
当然,并不是说存在中文等字符的字符串不能通过下标索引遍历,可以通过将字符串转换为 []rune
后遍历,通过这种方式我们也可以实现随机访问字符串元素。
s := "Hello, 世界"
runes := []rune(s)
for i := 0; i < len(runes); i++ {
fmt.Printf("字符 %d: %c\n", i, runes[i])
}
// Output:
字符 0: H
字符 1: e
...
字符 7: 世
字符 8: 界
在面对不同字符组成的字符串时,应根据实际场景选择遍历方式,正确获取元素的同时也提升代码性能。
- 纯 ASCII 文本:优先索引循环(性能最佳)
- 含多字节字符:使用
range
循环(平衡性能与正确性) - 需要随机访问字符:转换为
[]rune
(注意内存开销)
遍历方式 | 处理单位 | 多字节支持 | 内存开销 | 典型场景 |
---|---|---|---|---|
索引循环 | 字节 | ❌ | 最低 | 纯ASCII处理 |
range 循环 | Unicode | ✅ | 低 | 通用字符处理 |
转换为 []rune | Unicode | ✅ | 高 | 随机访问字符 |