从数据库取出一个中文标题,想拿前十个字做摘要,顺手写了 s[:10],打印出来末尾是半个乱码汉字。Go 的字符串是 UTF-8 字节序列,下标切的是字节,不是字符。
len 和字符数是两件事
len(s) 返回字节数,一个汉字占三字节,十个汉字就是三十。utf8.RuneCountInString(s) 才是字符数。做字数限制、算截断位置,先想清楚要的是哪个,两个数在纯英文里相等,混了中文就完全不同。
用 rune 切片
runes := []rune(s)if len(runes) > 10 { s = string(runes[:10]) }转成 []rune 会分配一块新内存,长度是字符数,切完再转回 string。短标题这样处理最省事。几十 KB 以上的长文本频繁转换会拖性能,可以只遍历前 N 个字符算出字节位置,用 utf8.DecodeRuneInString 逐个推进。
切片越界直接 panic
上面的判断不能省。字符不足十个时 runes[:10] 会 panic,这是线上最常见的低级事故之一。按字节切 s[start:end] 同理,end 超过 len(s) 一样崩,先夹住边界再动刀。
emoji 和组合字符
一个 emoji 可能是多个 rune,肤色、家庭这些由零宽连接符拼起来,按 rune 截断照样把表情劈成两半。要严格按用户看到的字符切,得用 golang.org/x/text 里的分段库,普通摘要场景不值得为一个表情引依赖。
常见需求的分工
判断前缀后缀用 strings.HasPrefix、HasSuffix,查找用 strings.Index,都在字节层面工作,纯英文和路径处理足够用,也不会产生额外分配。要按字符反转、按字符补空格对齐表格,就绕不开 rune。两条路别混着走,混了早晚出乱码。
结论:凡是按字数处理,先转 []rune;凡是拼 SQL、算字节、做 hash,才用 len。上线前拿几条真实标题跑一遍,正好等于上限的那几条最容易翻车。