Go 04:循环变量为什么会影响批量任务
把 03 的单次文本统计改为顺序批量执行后,for 循环已经出现。假如为了延迟读取任务标签,先在循环里保存几个函数,等循环结束再调用:它们会各自读到对应标签,还是都读到最后一个?Go 1.27.1 下,答案取决于 range 后面写的是声明 :=,还是向预先存在的变量赋值 =。无需启动 goroutine,这个差异就能稳定复现。
本篇仍沿用系列唯一累计工程。--batch 'one two|Go 写 Go' 暂时把 | 用作教学输入分隔符,按顺序统计两项;--text 原入口保留。此时没有 worker、排队、任务 ID 或持久化。本篇源码与证据归档包含截至 04 的完整工程与各章原始实验,旧章的历史输出不随新代码改写。
批量输入怎样保持可判断的结果
两个 flag 只能选其一。代码用 FlagSet.Visit 判断调用者实际提供了哪些选项,而不只看 --batch 对应字符串是不是空:显式传 --batch '' 与根本没传是两种输入,都不能变成一次看似成功的单任务。选择批量模式后,以 strings.Split(*batch, "|") 切出任务,在一个 for index, taskText := range tasks 循环中依次调用 03 的统计函数。
1 | |
具体实现用 strings.Builder 暂存待输出内容。如果第一项正常、第二项超过 --max-words,第一项已在内存中被统计,但进程不向 stdout 发布半份成功结果。--batch 'one two|one two three four' 实测标准输出为空、标准错误 word count 4 exceeds max-words 3,退出 2。中间有空项 Go||Go 也退出 2,stdout 为空。这是一项当前 CLI 的输出规则,不是计算回滚、数据库事务或外部副作用撤销;未来引入真正存储前不能照搬“输出为空”来推断“工作从未发生”。
当前每项各受字段上限 3 约束,但没有限制批次数量。整个批次的内容会驻留在内存字符串与 Builder 中;它既不是有界任务队列,也没有提供 | 转义。合法原文若真的包含竖线,当前 CLI 会拆错;解析与输入契约到 14 再处理,20 才加入有界并发,不因 CLI 已能批量循环就提前称为 worker pool。
两种 range 写法为何给出不同标签
下面两份不足一个完整工程的短程序位于本模块 testdata/,唯一关键差别是循环变量在哪里声明。testdata/loop-declare.go 的片段为:
1 | |
在冻结的 go 1.27.1 语言版本下,range 子句里的 := 每轮声明自己的 value。函数在循环完成后才调用,实测标准输出为 A B C,退出 0。代码没有 go 语句,因此不是“恰好三个 goroutine 按正确顺序运行”。
testdata/loop-assign.go 则先声明 var value string,循环改为 for _, value = range values。本版语言规范在这种形式下将迭代值赋给已有变量;三个保存的函数读的是同一个外层 value,循环结束后实际打印 C C C,进程同样退出 0。若原意是让每个函数保留各自对应的任务标签,这就是行为反例。:= 与 = 的差异不是字符串复制是否足够深,也不是 channel 的接收顺序,关键是闭包读取了哪一个变量绑定。
Go 语言规范注明迭代变量在 range 子句声明时每轮有新变量,并标注了 Go 1.22 的变化。这里同时固定 go.mod 中 go 1.27.1 和实测工具链 go version go1.27.1 linux/amd64;没有调用 Go 1.21 或更老的编译器,因此不把旧版本的预期写成跨版本实测。两份局部示例只证明该语言版本和这两种写法的结果;将来异步提交函数时仍需另行验证退出责任与同步边。
单次入口有没有被新循环改坏
go test -count=1 -v ./... 在当前 module 的根包与 stats 包均退出 0。根包测试除了原来的配置、文本单位与错误参数,还加入合法批量、中间空项、第二项超限、同时给 --text 与 --batch 等输入。编译二进制后实测:
| 输入 | stdout | stderr / 退出码 |
|---|---|---|
--text 'Go 写 Go' |
words=3 bytes=9 runes=7 |
空 / 0 |
| `–batch 'one two | Go 写 Go’` | task[0] words=2 bytes=7 runes=7 与 task[1] words=3 bytes=9 runes=7 两行 |
| `–batch 'Go | Go’` | |
| `–batch 'one two | one two three four’` | 空 |
同时给 --text Go --batch Go |
空 | 用法提示 / 2 |
这些是本次 Linux amd64 / Go 1.27.1 上的观测,双流、命令、GOTOOLCHAIN=local GOWORK=off GOMAXPROCS=2 CGO_ENABLED=0 和退出码保存在 writing-plans/go-language/evidence/04/。go run ./testdata/loop-declare.go 与 go run ./testdata/loop-assign.go 的两份原始输出也在该目录。两份独立入口放入 Go 忽略的 testdata/,日常 go test ./... 不会把它们合并成冲突的两个 main 函数。
若网站只带 source/ 而不带仓库根目录 examples/,可下载本章完整累计归档,SHA-256 为 53b5e376b589cf86e9bc981c41ba8f0c2307f6d0ee25156ad7251c1b2d54bb2e,并用校验文件复核。包内有 RUN/VERSIONS、工程与 00–04 Go 原始证据。本机已独立解包重跑两包测试、编译、正反批次及两种循环写法;素材目录 unpack-* 保存原始结果与退出码。此处没有发布网站,更没有发布一个后台任务服务。
练习一:为什么“第一个任务正常、第二个超上限,stdout 为空”仍不能证明第一个任务没执行?答案:strings.Builder 在写 stdout 之前已经接收了第一项的结果;只是异常返回时丢弃了那段内存输出。本章实测了外部可见输出和错误码,没有数据库状态或资源回滚的证据。
练习二:若任务 ID 在循环内用 := 声明,各闭包在循环之后读取什么?若事先声明变量再在 range 中赋值呢?答案:本版两个可运行例子分别输出 A B C 和 C C C,都以 0 退出。需要捕获每轮值时,先核对变量声明位置,再讨论后续的并发时序;仅用一次“没撞上错误”的异步执行不能建立语义保证。
下一篇在顺序任务路径加入明确的资源清理步骤,验证函数参数求值、返回与 defer 的顺序;本章只用到函数值延后读取变量,还没有讲清普通函数和清理动作的完整生命周期。
参考资料
- Go 语言规范:range 子句、短变量声明。语义核对所用版本快照为 Go 1.27.1 工具链附带的
doc/go_spec.html;源码 taggo1.27.1、Git SHA862c888e612ac346c7c4d99c9392bdfd265f33b0(Git SHA 来自 Go 镜像,官方 Gitiles 仍未交叉核验)。 flag.FlagSet.Visit、strings.Builder。它们在这里分别用于辨认显式选项与暂存 stdout,不能为真实数据库事务提供保证。






