Go语言中的字符串本质是不可变的字节序列,默认采用UTF-8编码存储,当字符串包含中文、 emoji等非ASCII字符时,直接使用切片遍历字节往往无法得到预期的字符结果,这时候就需要结合Rune类型来处理字符层面的操作。

Go语言字符串的底层存储逻辑
Go语言的字符串在底层由两部分组成,一部分是指向底层字节数组的指针,另一部分是字符串的长度。字符串的底层字节数组存储的是UTF-8编码后的字节,对于ASCII字符,每个字符占1个字节;对于中文等Unicode字符,每个字符可能占3到4个字节。因此如果直接对字符串做切片遍历,遍历的是单个字节,而非完整的字符。
字符串切片遍历的两种方式对比
字节遍历(直接切片遍历)
直接对字符串进行切片遍历,得到的是每个字节的值,对于非ASCII字符,会出现拆分的情况,示例如下:
package main
import "fmt"
func main() {
s := "hello世界"
// 直接遍历字符串,得到的是字节
for i := 0; i < len(s); i++ {
fmt.Printf("索引%d: 字节值%d 字符%cn", i, s[i], s[i])
}
}
上述代码的输出中,中文“世”和“界”会被拆分成多个字节,无法正确显示完整的字符。
Rune遍历(字符层面遍历)
使用for range遍历字符串时,Go会自动将字符串解码为Rune序列,每个Rune对应一个完整的Unicode字符,示例如下:
package main
import "fmt"
func main() {
s := "hello世界"
// for range遍历字符串,得到的是rune(Unicode字符)
for index, r := range s {
fmt.Printf("索引%d: rune值%d 字符%cn", index, r, r)
}
}
这段代码的输出中,中文“世”和“界”会被作为完整的字符处理,索引也会对应每个字符的起始字节位置。
Rune类型在字符打印中的实践
Rune类型是Go语言内置的int32类型的别名,用来表示一个Unicode码点。当我们需要单独获取字符串中的某个字符并打印时,可以先将字符串转换为Rune切片,再进行操作。
字符串转Rune切片
通过类型转换可以将字符串直接转为Rune切片,此时切片中的每个元素都是一个完整的Unicode字符:
package main
import "fmt"
func main() {
s := "Go语言教程"
// 将字符串转换为rune切片
runes := []rune(s)
fmt.Printf("字符串长度(字节): %dn", len(s))
fmt.Printf("rune切片长度(字符数): %dn", len(runes))
// 打印每个rune字符
for i, r := range runes {
fmt.Printf("第%d个字符: %c, Unicode码点: %Un", i+1, r, r)
}
}
单个Rune字符的打印
如果只需要打印字符串中的某个特定字符,可以通过Rune切片索引获取,也可以直接对字符串做Rune转换后取值:
package main
import "fmt"
func main() {
s := "Hello, 世界"
// 获取第三个字符(索引从0开始)
runes := []rune(s)
if len(runes) > 2 {
fmt.Printf("第三个字符是: %cn", runes[2])
}
// 直接转换单个字符
r := rune(s[0]) // 注意:这种方式仅适用于ASCII字符,非ASCII字符直接用s[i]取的是单个字节
fmt.Printf("第一个字节对应的字符: %cn", r)
}
常见注意事项
- 不要直接用
len(s)获取字符串的字符数,len(s)返回的是字符串的字节长度,不是字符数,字符数需要通过len([]rune(s))获取。 - 对包含非ASCII字符的字符串做切片操作时,要基于Rune切片进行,不要直接对原字符串做切片,否则可能拆分Unicode字符。
for range遍历字符串时,每次迭代的索引是字符的起始字节位置,不是连续的1递增,这点需要注意。
总结
Go语言字符串的切片遍历和字符打印需要结合底层UTF-8编码的特性来处理,对于纯ASCII场景,直接切片遍历即可;对于包含多语言字符的场景,优先使用for range遍历或者转换为Rune切片后再操作,这样才能保证每个字符的完整性。Rune类型作为Unicode码点的载体,是处理多语言字符串的核心类型,掌握它的使用可以有效避免字符串处理中的乱码和拆分问题。