07 用临时 map 把 task[0] 映射到结果 slice 的第 0 项。计数、ID 和状态要放在同一个任务记录里时,先复制记录留作快照,再更新原记录,快照会变吗?答案取决于记录里的字段怎样保存数据,也取决于更新方法的接收者。把 Go 的嵌入写成「类继承」会掩盖这个问题。

同一累计工程引入 TaskRecord:包含 ID、TaskResult 和 Status,后者当前只使用 QUEUED 和 SUCCEEDED。原 CLI 的文本、文件、批次与本次批次的查询仍保持原输出。新增的类型只组织顺序计算的结果,尚未引入 worker 或对外可查询的持久任务;本篇完整归档和SHA256SUMS用于离线复跑。

独立复跑步骤列出本章正反例、源码校验及运行命令。

更新原件,快照保持什么

newTaskRecord("task[0]") 返回值的 ID 与初始 QUEUED 状态。record := newTaskRecord(...) 得到可寻址的局部变量;snapshot := record 复制此时整个结构体值;然后 record.MarkSucceeded(counts) 经指针接收者修改原变量的 TaskResult 和 Status。

1
2
3
4
func (record *TaskRecord) MarkSucceeded(counts stats.Counts) {
record.TaskResult = TaskResult{Counts: counts}
record.Status = StatusSucceeded
}

TestTaskRecordCopyAndPointerReceiver 断言原件被更新后 record.Status == SUCCEEDED 且 record.Words == 1,先前快照仍为 QUEUED、Words == 0。再复制一个已完成的记录,只更新新副本,旧记录仍为 1,新副本为 2。本章的 stats.Counts 只有三个整数,ID 和状态都是字符串值,因此这个结构体复制可以支持上述快照结论;日后如果在结构体里加入 slice、map 或指针,复制外层结构体不会递归复制所指的数据。06 的底层数组实验正是这一限制的直接对照。

MarkSucceeded 的指针接收者能修改调用方持有的记录。根包测试中 record.MarkSucceeded(...) 可以通过编译,因为 record 是可寻址变量,调用时可自动取址;这不等于所有 TaskRecord 表达式都能调用指针方法。独立的 testdata/nonaddressable-receiver.go 使用同一类型形状的最小短程序,对函数返回的临时 record 值直接调用指针方法:go build 实测退出 1,stderr 为 cannot call pointer method markSucceeded on record。该示例不跟正常 CLI 合并编译;要调用此方法,可先把函数返回值赋给变量。正常根包测试的编译期断言还检查 *TaskRecord 拥有 MarkSucceeded,而 TaskRecord 值拥有下文提升的 Summary。

嵌入带来怎样的字段和方法查找

TaskResult 嵌入 stats.Counts,用值接收者定义 Summary() string,格式化三个计数;TaskRecord 再嵌入 TaskResult。成员查找链可以写成:

1
2
3
record.Summary()  -> record.TaskResult.Summary()
record.Words -> record.TaskResult.Counts.Words
record.Status -> TaskRecord 自己的字段

record.Summary() 的测试返回 words=1 bytes=2 runes=2;按本版 Go 规范,嵌入的值类型上的方法可被提升到外层值和指针的方法集。这里是选择器查找和方法集规则,不意味着 TaskRecord 是 TaskResult 的子类,更没有建立覆写或动态派发的继承层级。如果另写一个同名方法、引入有冲突的嵌入字段,选择器规则还需要重新分析;本章没有靠这些未执行情况解释现有输出。

保持旧入口的输出契约

顺序 CLI 先完整校验各条文本与上限,随后为每条有效文本构造 TaskRecord,在同一调用栈立即标记 SUCCEEDED,再写入 records slice。indexTasks(records) 使用记录里的 ID 建当前批次索引;--lookup 命中后读取记录,批量默认输出和单文本输出经提升的 Summary 格式化。它保留了 04 的 stdout 暂存契约:后项非法时不输出先前成功行。状态目前只是这个顺序构造过程里的字段,不可用来推断后台任务已经由其他 worker 执行、退出或提交数据库。

在本机 Linux amd64、Go 1.27.1、GOTOOLCHAIN=local GOWORK=off GOMAXPROCS=2 CGO_ENABLED=0 下,go test -count=1 -v ./... 两个包退出 0。独立记录测试与 07 的索引测试退出 0;错误接收者样例编译退出 1。编译后的主 CLI --text 'Go 写 Go' 返回 words=3 bytes=9 runes=7;--batch 'Go|one two'、查询 task[0]、删除后追加均与原输出一致;缺失 ID 仍走 stderr 并退出 2。实际命令、环境、双流及退出码在 writing-plans/go-language/evidence/08/,解包后的再次运行结果在本篇素材 unpack-*。不能因为原单线程测试无报错,就称未来并发访问 TaskRecord 安全。

练习:如果把 MarkSucceeded 改成值接收者,代码仍可编译;为什么 record.Status 不会成为 SUCCEEDED?答案:调用此方法修改的是传入接收者的副本,方法返回后原变量仍保留旧状态。编写测试时不能只检查方法内部看到的值,必须断言调用者的 record。如果只是打印计数、无需更新原记录,TaskResult.Summary 的值接收者反而符合这里的目的。

下一篇从消费方要求出发为查询存储提取最小接口,验证值方法和指针方法对接口实现的影响。当前服务没有 HTTP、SQL、真正的排队与取消,race 检测、其他平台和发布均 NOT_RUN。Go 源码 tag go1.27.1,Git SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 来自 Go 镜像,官方 Gitiles 尚未交叉核验。

参考资料