01 把统计函数移出了 main 包,但任何非空文本都可以被统计。现在给任务增加“最多允许几个字段”的配置:默认允许 3 个,调用者也能通过 --max-words 指定上限。一个易错的写法是把“没有写上限”和“上限等于零”混为一谈;另一个是以为代码在内部作用域给同名变量重新赋值,实际上创建了新变量。两类问题都会让程序执行与调用者预期不符。

这仍是同一个顺序 CLI 的递进版本,没有任务队列或数据库。02 累计工程与证据包含此前工程、当前配置实现和 00–02 原始实验输出。00 和 01 的原代码也各自在当章素材包中保留,不用最新的实现覆盖历史结果。

零值不能暗中充当默认配置

Config 目前只含一个整数:

1
2
3
type Config struct {
MaxWords int
}

Go 的整型字段未经赋值时是 0,因此 Config{} 的 MaxWords 是 0。这个值在本任务里不是“无限制”,也不是“自动用 3”:0 表示无效配置。validateConfig(Config{}) 返回 max-words must be positive;Config{MaxWords: 3} 则通过。对应的单元测试还断言负数被拒绝。把零值识别为错误的选择由业务契约决定,整型字段初始为 0 才是语言保证。

CLI 在定义标志时显式写 flags.Int("max-words", 3, ...)。用户不传该选项,flag 包采用这个指定的默认数 3;显式写 --max-words 0,解析所得就是 0,而非默认值。go.mod 或 Go 版本都不会替 CLI 猜测零是不是“没设置”。如果以后确需区分省略选项与显式给 3,还要另外保存“是否出现”这个事实;本章只要求两种输入得到相同的有效上限,不把这种更强的区分伪装成已有能力。

1
2
3
4
5
flag 解析成功
└─ 创建 Config ── 非正数?──是──> stderr + 退出 2
└─否──> 检查文本非空 ──> stats.CountWords
└─超上限?──是──> stderr + 退出 2
└─否──> stdout + 退出 0

main.go 先调用 validateConfig,再检查文本并执行统计。配置非法时,错误会在本次顺序计算前返回。超上限则不同:程序只有调用 stats.CountWords 才知道结果是 4,因此“超过允许的字段数而拒绝成功输出”不等于“完全没有进行统计”。图中的两种退出 2 不能归为同一个执行时点。未来把统计换成有成本的任务时,这个区别会影响是否能提前拦截输入;当前章没有声称实现按字节限额的提前截断。

同名变量为什么没改变外层限制

testdata/shadow.go 与 testdata/assign.go 是同一 module 内两份只有十来行的局部对照,不是两套任务服务。第一份在内层 if 块用了短变量声明:

1
2
3
4
5
6
7
maxWords := 3
requested := 1
if requested > 0 {
maxWords := requested
_ = maxWords
}
fmt.Printf("max-words=%d\n", maxWords)

:= 在内层块引入新的 maxWords;块外原变量仍是 3。这个短程序真实输出 max-words=3,进程退出码却是 0。它正说明“命令执行成功”不能证明“调用者请求的限制生效”。第二份把内层一行改为 maxWords = requested,实际输出 max-words=1,退出码也为 0。两段代码对照的是变量绑定,不涉及指针别名或 goroutine 竞争;用并发问题解释这个差异只会掩盖词法作用域。

累计 CLI 并没有把故障示例接进去。真正的处理路径用 Config{MaxWords: *maxWords} 把解析结果带到外层,验证后读取同一个 config.MaxWords。根目录 main_test.go 对显式 --max-words 1 --text Go 断言输出 words=1;对 --max-words 0 断言非零退出和无成功输出。单独的遮蔽程序保留在 testdata/,不会因为执行 go test ./... 被误当作另一个主包,也不向主工程泄漏示范用的错误声明。

失败路径和旧契约一起检查

本章仍使用冻结的 Go 1.27.1、Linux amd64、GOTOOLCHAIN=local、GOWORK=off、GOMAXPROCS=2、CGO_ENABLED=0,没有引入外部依赖。进入 examples/go-language/ 后运行 go test -count=1 -v ./...,两个包的测试均退出 0,包含配置零值、正数、负数以及原先的统计和 CLI 用例。用 go build -o /tmp/go-language-02-taskstats . 编译后分别执行:

输入 标准输出 标准错误要点 实测退出码
--text 'Go 写 Go' words=3 空 0
--text Go --max-words 1 words=1 空 0
--text 'Go 写 Go' --max-words 0 空 max-words must be positive 2
--text Go --max-words -1 空 max-words must be positive 2
--text 'one two three four' 空 word count 4 exceeds max-words 3 2

以上来自本次执行的 writing-plans/go-language/evidence/02/,各条 .command、.stdout、.stderr、.exit 文件逐项保存原始值。TestRun/invalid_config_before_empty 用“空白文本与零上限同时出现”确认实际报的是配置错误:这为本版入口中的检查顺序提供证据。正常完成和退出码只是当前这些输入的观察;未来要验证资源消耗,不能仅凭这张表断言超限路径没有处理成本。

博客部署不保证同时提供仓库根目录的 examples/。本章归档 SHA-256 为 cfe325e84e6fd75bf25ce7ececf1b3fde3edb0022fe908c4b229bbb4cfe053fc,SHA256SUMS 可独立核对。归档包含当前完整工程、RUN.md/VERSIONS.md、本次证据与前章证据;本机已解包重跑测试、构建、默认成功、非法上限、超限及两份遮蔽对照,原始结果在素材目录的 unpack-*。这验证包里不是只有正文,却不表示已对其他操作系统、race 或性能做过验收。

练习一:Config{} 与用户不传 --max-words,是否都会得到同一个数?答案:前者字段是 0,validateConfig 拒绝;后者由 flag.Int 明确给出默认 3,正常文本可以处理。这是语言零值和 CLI 默认值的不同来源,单元测试和默认输入均已实测。

练习二:把遮蔽示例里的 maxWords := requested 换成 maxWords = requested,外层输出会怎么变?答案:在本例变量已于外层声明,赋值使打印结果由 3 变为 1。两份短程序本次都执行过;如果新建的不是 if 块而是另一个函数,变量可见范围需要重新按该处源码判断,不能外推本例。

下一篇继续使用当前 Config 与 CLI,明确字节数、码点数和本章字段数各是什么,给 ASCII、中文和空串固定不同的断言;本章“词”只是按空白分段的教学口径,尚未实现自然语言分词。

参考资料