for range遍历字符串得到的是rune而非byte;i为字节起始位置,r为Unicode码点;str[0]取首字节非首字符;len(str)返回字节数非字符数。

for range 遍历字符串得到的是 rune,不是 byte
Go 的 string 底层是 UTF-8 编码的字节序列,但 for range 会自动按 Unicode 码点(即 rune)拆分,而不是按字节。这意味着中文、emoji 或带重音的字母(如 "é")会被正确识别为单个字符,而非多个 byte。
常见错误是误以为 for range 返回的是下标和 byte,结果对中文做 str[i] 操作时 panic 或读到乱码。
-
for i, r := range "你好"中,i是该rune在字节串中的起始位置(0、3),r是rune类型的 Unicode 码点(如'你'对应20320) - 直接用
str[0]取的是第一个字节(UTF-8 编码首字节),不是第一个字符 - 若需字节索引或操作原始字节,应先转
[]byte(str),但注意这会丢失 Unicode 语义
想获取字符位置和长度?别依赖 len(str) 做循环边界
len("你好") 返回的是字节数(6),不是字符数(2)。用 for i := 0; i 配合 str[i] 会破坏 UTF-8 编码,导致非法字节序列或截断字符。
正确做法始终是用 for range,它内部已处理 UTF-8 解码逻辑:
立即学习“go语言免费学习笔记(深入)”;
str := "Hello 世界 ?"
for i, r := range str {
fmt.Printf("pos %d: %c (U+%04X)\n", i, r, r)
}
输出中 i 是字节偏移(0、5、8、12),r 是完整字符。如果真需要字符序号(第几个 Unicode 字符),可手动计数:
- 声明一个
idx := 0变量,在每次range迭代时递增 - 不要用
i当字符序号——它只是字节位置
遍历时修改字符串内容?不行,string 是不可变的
Go 中 string 是只读底层数组的引用,任何“修改”都必须新建字符串。试图在 for range 中赋值 str[i] = 'x' 会编译报错:cannot assign to str[i]。
常见替代方案:
- 转成
[]rune:适合需要逐字符编辑(如大小写转换、过滤 emoji),再用string(runes)转回 - 用
strings.Builder:适合拼接、条件跳过、插入等场景,性能更好 - 避免边遍历边构造新字符串(如
s += newPart),会触发多次内存分配
例如把每个汉字转成 ASCII 描述:
runes := []rune("Go编程")
var b strings.Builder
for _, r := range runes {
if unicode.Is(unicode.Han, r) {
b.WriteString(fmt.Sprintf("[U+%04X]", r))
} else {
b.WriteRune(r)
}
}
result := b.String() // "Go[U+7F16][U+7A0B]"
性能敏感场景:for range 比 for i
两者底层开销不同:for range 需要 UTF-8 解码,for i 直接访问字节。单纯遍历字节确实更快,但代价是无法正确处理多字节字符。
真实项目中,绝大多数字符串操作(显示、解析、校验)都需要语义正确的字符边界。此时 for range 是唯一安全选择;刻意绕过它去优化微秒级差异,往往引入 bug 的成本远高于收益。
只有极少数情况可考虑字节遍历:
- 确定字符串只含 ASCII(如 HTTP header 名、base64 数据)
- 做底层协议解析(如自定义二进制协议封装在 string 中)
- 且已通过
utf8.ValidString(s)排除无效 UTF-8
否则,老老实实用 for range —— 它不是语法糖,是 Go 对 Unicode 支持的核心设计。










