Go 03:byte、rune 和字段数怎样区分
02 的 CLI 把 Go 写 Go 计为 3 个“词”。如果还要报告文本大小,是否能直接把这个 3 用作长度?不能。三个用空白分隔的字段、UTF-8 编码占用的字节、字符串解码得到的码点,回答的是三种不同问题。中文输入让混用立即露馅:本次实验中它们分别是 3、9、7。
本篇只给同一个任务工程的统计结果加两列,不更换 CLI,也不把“码点数”改名叫“字符数”。完整累计工程与原始证据保留到 03 的源码、RUN/VERSIONS 和 00–03 实验;前章独立快照仍可复跑历史输出。
文本如何经过三把不同的尺子
本版 stats.Analyze 返回三个带名字的整数:
1 | |
Go 字符串是一段字节序列。对字符串使用内置 len 得到字节数,不会替调用者解码 Unicode。byte 是 uint8 的别名;rune 是 int32 的别名,惯用于表示 Unicode 码点。这里 utf8.RuneCountInString 按 UTF-8 解码后的 rune 数计数;CountWords 仍调用 strings.Fields 按 Unicode 空白划分字段。三个结果都用 Go 的 int 记录数值,但整数类型相同并不让单位相同:--max-words 只比较 Words,不会误用 Bytes 拒绝中文输入。
以 Go 写 Go 为例,ASCII 字母与空格各占一字节,“写”在 UTF-8 中占三个字节。按实际输入排列如下:
1 | |
这是所选 ASCII 与中文字符串的计算,不是 Go 编译器替所有文本定义了“词”。没有空白的“中文文本”在当前 strings.Fields 口径里只有一个字段;不同 Unicode 空白是否构成分隔由标准库函数的公开契约决定。字段的长度与结果中的 Bytes 也不是同一个量。
再看由 U+0065(拉丁字母 e)与 U+0301(结合锐音)顺次组成的输入,按本次原始字节编码,它占 3 字节、解码为 2 个 rune、没有空白所以是 1 个字段。显示器上可能看起来像一个带重音的字形,但渲染、规范化和字素簇分段都不是 RuneCountInString 提供的契约。不能把返回值 2 抄成“用户看见两个字符”。本章没有实现 Unicode 字素簇统计,更没有按自然语言词典切分。
空串为什么一处是零,一处是错误
统计包本身允许分析空字符串,stats.Analyze("") 的三个值都为 0;TestAnalyze/Empty 已断言这个结果。CLI 延续 00 的输入规则:--text '' 或纯空白被视为没有提交有效任务,在调用统计包前返回用法提示和退出码 2。两个行为并不冲突:一个描述纯函数在空输入上的数学结果,另一个描述对外入口接不接受这个输入。
这个区分以后仍重要。收到一个 HTTP 请求、得到一个数据库记录与接受一个真正可执行的任务,属于不同阶段;不能用一个底层函数“能算出 0”来证明上层“应该接受空任务”。目前服务、数据库和并发执行器尚未实现,这里只依据 CLI 的实际调用顺序解释边界。
哪些差异真正跑出来了
Go 1.27.1 / Linux amd64 下,使用 GOTOOLCHAIN=local GOWORK=off GOMAXPROCS=2 CGO_ENABLED=0,执行 go test -count=1 -v ./...;统计包对 ASCII、中文、空串、结合符四组输入分别检查 Counts,入口测试继续覆盖上限与错误参数。两个包测试退出 0,编译 go build -o /tmp/go-language-03-taskstats . 也退出 0。随后直接运行二进制,避免把 go run 的外层状态当作业务程序的退出码:
| 输入 | 原始 stdout | stderr / 退出码 |
|---|---|---|
--text 'one two' |
words=2 bytes=7 runes=7 |
空 / 0 |
--text 'Go 写 Go' |
words=3 bytes=9 runes=7 |
空 / 0 |
--text 后接 U+0065、U+0301 |
words=1 bytes=3 runes=2 |
空 / 0 |
--text '' |
空 | 用法提示 / 2 |
--text 'Go 写 Go' --max-words 1 |
空 | word count 3 exceeds max-words 1 / 2 |
原始命令、环境、标准输出、标准错误和退出码存放在 writing-plans/go-language/evidence/03/。空串的三项零值来自统计包测试,不来自 CLI 成功输出;表中空串的 stdout 确实为空。与 02 相比 CLI 正例新增 bytes、runes 两列,旧文章保留此前的输出与归档,不拿最新程序回填早期实测。
仅在隔离副本里,把 Runes: utf8.RuneCountInString(text) 故意改成等价于 Runes: len(text) 的表达式,并保留 utf8 导入以确保不是“未使用的 import”这种编译错误。相同测试退出 1:日志具体报告 Analyze("Go 写 Go") 算出 Runes:9 而期望 7,结合符输入算出 3 而期望 2。ASCII 输入的字节数和码点数都为 7,因此单测 ASCII 无法发现这处混用。故障代码的唯一修改在 rune-as-byte.diff,累计工程仍使用标准库的 rune 计数并通过测试。
标准库对无效或截短的 UTF-8 编码也有计数规则,但本章没有向 CLI 注入无效字节。几组有效输入的结果不能覆盖任意字节序列,更不能推断用户可见字符数;那还需要明确字素簇算法与 Unicode 版本。
复跑包与练习
站点只保证正文素材随 source/ 出现在可部署内容里。本章归档 SHA-256 为 a3d038eb2dce3a9a39d6e7f1568d6d529b76f77d78011656c78544c405866512;SHA256SUMS 可在下载后校验。包内 RUN.md、VERSIONS.md、完整源码及各章原始实验均存在;在本机独立解包后已重新运行测试、构建、中文和结合符成功输入与空串失败输入,退出码和双流存于素材目录 unpack-*。不把归档存在当作其他平台已运行的证明。
练习一:为何只测 ASCII 的 one two 不能证明 rune 统计正确?答案:本次这个输入的 7 个字节各恰好占一个码点,混用 len(text) 也会得到 7;中文和结合符的断言才能区分。故意改错后的测试输出已有实证,不是猜想。
练习二:stats.Analyze("") 与 CLI --text '' 的结果各是什么?答案:统计包断言 {Words:0 Bytes:0 Runes:0};CLI 在输入校验处退出 2,stdout 空。这是两个不同入口,不能把后者改写成“实测输出三个 0”。
下一篇在同一 CLI 上增加顺序批量任务。那时循环变量若被闭包持有,必须按冻结的 Go 语言版本以及声明形式分别验证,不能借 03 的码点示例推断循环作用域。
参考资料
- Go 1.27.1 语言规范:String types、内置函数 len、预声明标识符。本文规范主张以实际工具链内的
doc/go_spec.html为版本快照。 unicode/utf8.RuneCountInString与strings.Fields的公开契约;前者在 Gogo1.27.1对应src/unicode/utf8/utf8.go:447的RuneCountInString,tag Git SHA862c888e612ac346c7c4d99c9392bdfd265f33b0(源码 Git SHA 来自 Go 镜像,官方 Gitiles 交叉验证仍未完成)。






