len() 返回字节长度而非字符数;utf8.runecountinstring() 才返回真实字符数,安全高效,适用于校验、截断等场景,且对非法utf-8具容错性。

len() 返回的是字节长度,不是字符个数
Go 的 string 本质是只读的 UTF-8 字节序列,len() 拿到的就是底层 []byte 的长度。ASCII 字符(如英文、数字)一个字节一个 rune,所以 len("abc") 和真实字符数一致;但中文、emoji、带重音的字母(如 café 中的 é)在 UTF-8 中占 2–4 字节,len("你好") 是 6,不是 2。
常见错误现象:
- 用
len(s) 做昵称长度限制,结果用户输 “我” 就占了 3 字节,实际只算 1 个字符,却提前被截断或拒绝 -
s[5]直接取第 6 个字节,可能落在某个汉字中间,导致string(s[5])输出或 panic -
strings.Split(s, "")得到的是字节切片,不是 rune 切片,拆出来一堆非法 UTF-8 片段
utf8.RuneCountInString() 才是真正的“人眼可见字符数”
这个函数遍历 UTF-8 字节流,逐个解码 rune,只计数、不分配内存,性能好且安全。它返回的是 Unicode 码点数量,也就是用户感知的“字符个数”。
使用场景和注意事项:
- 做输入校验:用户名 ≤ 12 个字符?用
utf8.RuneCountInString(name) ,别用 <code>len(name) - 日志截断:要取前 20 个字符 +
"…",得先用utf8.RuneCountInString(s)判断总长,再用for range或utf8.DecodeRuneInString安全截取,避免切在汉字中间 - 和
len([]rune(s))的区别:后者会拷贝整个字符串为 rune 切片,大字符串(如几 MB 日志)可能引发 GC 压力;前者零分配,推荐优先使用
遍历时必须用 for range,不能用 for i := 0; i
后者按字节索引走,遇到多字节字符就会错位。比如 s := "你好",len(s) 是 6,循环执行 6 次,但 s[2] 是第一个汉字的第二个字节,单独打印就是乱码。
正确做法只有这一种:
- 遍历每个字符:用
for _, r := range s,r是rune类型,每次拿到完整 Unicode 码点 - 需要知道字节偏移?
for i, r := range s中的i是起始字节位置,不是字符序号 - 真要按序号取第 n 个字符?先转
rs := []rune(s),再rs[n]—— 但注意这会分配新内存,高频或大数据量时谨慎
含无效 UTF-8 序列时,utf8.RuneCountInString 仍能工作
它对非法字节序列有容错:遇到无法解码的字节,会把那个字节当作一个 rune(U+FFFD),继续往后解析。所以即使输入混入二进制垃圾,也不会 panic,只是计数略偏高。
如果必须严格校验有效性,要用 utf8.DecodeRuneInString 配合循环,手动检查每个 rune 是否为 utf8.RuneError。
容易被忽略的一点:很多 JSON 解析库、HTTP body 读取默认不做 UTF-8 验证,上游传来的字符串可能带损坏编码——这时候 utf8.RuneCountInString 的容错性反而是优势,而 []rune(s) 在遇到非法序列时会直接 panic。











