Go 里的中文字符串:长度、截断和按字节出错的写法
程序的默认语言是英文,代码里的每一处字符串处理也是。等到界面里出现中文,len()、切片、strings.Title 这些看起来很基础的调用就会开始出错,而且错得很安静:要么截出半个汉字,要么标题只截了二十个字却显示成一排。
这篇记几个实际会踩的点。
一、len() 是字节数,不是字数
Go 的字符串本质是只读的字节序列。len("你好") 是 6,不是 2,因为一个汉字在 UTF-8 里占三个字节。
想数字符要用 utf8.RuneCountInString,或者 len([]rune(s)):
fmt.Println(len("你好")) // 6
fmt.Println(utf8.RuneCountInString("你好")) // 2
这不是冷知识。所有「最多多少个字」的限制,判断时都该用 rune 数。用字节数判断会出现「英文能写 100 个字符,中文只能写 33 个」的怪现象。
二、截断必须按 rune
下面这行代码在英文下永远正确,在中文下会截出乱码:
short := long[:100]
long[:100] 是按字节切,如果第 100 个字节正好落在某个汉字中间,结果就是一个非法 UTF-8 序列,打印出来是 �,写进 JSON 之后可能直接报错。
正确做法是先转成 rune 切片:
r := []rune(long)
if len(r) > 100 {
short = string(r[:100])
}
代价是一次内存拷贝。对一个几百字的摘要来说完全可以忽略;如果是大文本反复截断,才需要考虑用 utf8.DecodeRune 边走边数。
三、宽度和长度是两回事
长度是字符数,宽度是显示出来占几列。中文和中文标点在终端里占两列,所以「长度一样」的两行字,对齐结果完全不同:
名字 长度 4
abcdef 长度 6
上面两行如果按长度补空格,第二行会比第一行长——因为第一行四个汉字实际占了八列。
终端对齐需要按显示宽度算,标准库没有直接支持,golang.org/x/text/width 或者 go-runewidth 这类包能做。项目不引依赖的话,退一步的做法是:手动给含中文的列留固定宽度,或者干脆别用表格。
四、拼接用 Builder,别用 +
少量拼接用 + 没问题,循环里用 + 会不断分配新字符串,复杂度是平方级的:
var b strings.Builder
for _, s := range parts {
b.WriteString(s)
b.WriteString("\n")
}
result := b.String()
strings.Builder 还会避免拷贝转换,b.WriteString 传字符串不需要先转成 []byte。这类细节在小工具里不关键,但写成习惯没有坏处。
五、大小写、比较和规范化
strings.ToUpper、strings.EqualFold 对中文不起作用,这没问题。真正的坑在别处:
中文输入法打出的全角字母、数字、标点和半角是不同的码点。用户输入的 A 和 A 不相等,123 和 123 也不相等。如果要做「按名称查找」,得先做一次全角转半角。
中文标点同理:, 和 ,、: 和 :。做关键词匹配前统一一下,能少很多「明明输入了却搜不到」的问题。
繁简是另一个维度。strings 包完全不处理,需要专门的转换表。这一条除非明确需要,否则不建议自己写。
六、小结
四条规则:
1. 数字数用 rune,不用字节; 2. 截断先转 []rune,别直接切字符串; 3. 终端对齐按显示宽度算,不是字符数; 4. 全角半角、中文标点在比较前先统一。
这些都不会在英文环境下暴露,所以测试和自测的时候,记得拿一段中文去试——哪怕只是把测试数据里的 hello 换成 你好。
- 点赞
- 收藏
- 关注作者
评论(0)