按字节遍历字符串会乱码,因为go字符串底层是utf-8字节数组,string是只读字节序列([]byte封装),非字符数组,需用for range按rune遍历。

按字节遍历字符串会乱码,因为 Go 字符串底层是 UTF-8 字节数组
Go 中 string 是只读的字节序列([]byte 的封装),不是字符数组。直接用 for i := 0; i 取 <code>s[i] 拿到的是单个字节,对中文、emoji 等多字节 UTF-8 编码会截断,输出乱码或 panic(如越界访问)。
常见错误现象:s := "你好"; fmt.Printf("%x", s[0]) 打印出 e4(UTF-8 第一字节),而非“你”的完整编码;若误用 s[2] 还可能越界。
- UTF-8 中 ASCII 字符占 1 字节,中文通常占 3 字节,emoji 常占 4 字节
- 按字节索引仅适合处理纯 ASCII 或明确已知编码边界的数据(如协议头、base64 片段)
- 想安全提取子串?别用
s[2:5]直接切,先确认起止位置是否落在合法 UTF-8 字符边界上(可用utf8.RuneStart()检查)
按字符(rune)遍历必须用 for range,不能用传统的 for i 循环
for range 是 Go 唯一内置支持 UTF-8 安全迭代的方式:它自动解码字节流,每次迭代返回一个 rune(Unicode 码点)和该字符在字符串中的起始字节索引。
正确写法:
for i, r := range s {
fmt.Printf("index %d: rune %U\n", i, r)
}
-
i是字节偏移量,不是字符序号(例如"世?"中 “?” 的i是 3,因为 “世” 占 3 字节) -
r是rune类型,可直接参与 Unicode 处理(如unicode.IsLetter(r)) - 不要试图用
for i := 0; i 再去“按字符索引”,这效率低且无法获得字节位置
需要高效获取所有 rune 切片?先转换再遍历,但注意内存与场景权衡
如果逻辑需多次随机访问字符(如反转、取第 N 个字符),把字符串转成 []rune 是合理选择:
立即学习“go语言免费学习笔记(深入)”;
rs := []rune(s)
for i, r := range rs {
// i 是字符序号,r 是 rune
}
- 转换开销:一次遍历解码 + 分配新底层数组,时间 O(n),空间 O(n);对长字符串(MB 级)要谨慎
- 优势:后续所有索引操作都是 O(1),且
len(rs)就是字符数,语义清晰 - 替代方案:若只需首/尾字符,用
utf8.DecodeRuneInString(s)和utf8.DecodeLastRuneInString(s)避免全量转换
高性能场景慎用 strings.FieldsFunc 或正则拆分中文字符串
对含中文的字符串做分词、按标点切割时,容易掉进性能坑:比如 strings.FieldsFunc(s, unicode.IsSpace) 或 regexp.MustCompile(`\p{Han}+`).FindAllString(s, -1),看似简洁,实则每次调用都重新扫描、分配、解码。
- 高频调用(如日志解析、HTTP 请求体处理)下,优先手写
for range状态机,复用变量,避免中间字符串分配 - 若必须用正则,编译一次全局复用,且注意
\p{Han}匹配所有汉字,但不包含中文标点;更准的分词建议用专用库(如gojieba) - 简单按字符处理逻辑,90% 场景用
for range足够,别过早抽象成工具函数增加间接成本










