Go 07:map 的零值为什么不能表示任务不存在
06 的批次按顺序保存在 slice 中;删除第一项并添加新项之后,调用者可按 task[0]、task[1] 看到当前批次的编号。如果另存一张 map[string]int 记录 ID 到切片位置的映射,positions["task[0]"] 得到 0。查询不存在的 ID 也可能得到 0:仅靠一次索引无法说明第一项真的存在。
同一累计工程现在给 --batch 加入互斥的 --lookup ID 和 --list-ids。它只是一次 CLI 调用期间建立的索引,不保存跨进程的任务身份,也没有后台 worker。原 --text、--file、顺序 --batch 与 06 的增删用法仍有效。完整累计工程与原始结果和SHA256SUMS可独立下载复跑。
独立复跑步骤列出本章正反例、源码校验及运行命令。
第 0 项与缺失键的共同返回值
indexTasks 用 make(map[string]int, count) 初始化映射,遍历当前批次,为每个位置写入 task[0]、task[1] 等键;这里的 count 是容量提示,不是固定的最大可添加条数。查找时必须保留两个结果:
1 | |
TestIndexTasks 用 12 个位置同时断言 task[0] 得到 position=0, found=true,而 task[missing] 得到 position=0, found=false。后者的 0 是 int 的零值,不是查询成功。根包 TestRun 和编译二进制的实测也分别覆盖 --batch 'Go|one two' --lookup 'task[0]'(task[0] words=1 bytes=2 runes=2,退出 0)及 --lookup 'task[missing]'(stdout 空、stderr task not found: task[missing],退出 2)。命令、双流、退出码在 writing-plans/go-language/evidence/07/lookup-zero.* 和 lookup-missing.*。
本章先对整个输入批次验证并统计,再建本次查询索引。--batch 'Go|' --lookup 'task[missing]' 首先报空文本输入而不是缺失 ID;无论前面已分析多少项,都不写成功 stdout。这是本次顺序入口的错误优先级及输出暂存选择,并不能说明已分析的部分任务发生了回滚。查询只是从已验证的 results slice 中取回计数,没向数据库写入任何可恢复的任务记录。
nil map 为什么能读却不能写
Go 语言规范将未初始化 map 的值定义为 nil;读不存在的键可得到元素零值和 ok=false,向 nil map 加元素则不允许。indexTasks 明确 make 后才赋值。独立短程序 testdata/nil-map-write.go 故意声明 var positions map[string]int 后直接执行 positions["task[0]"] = 0。在本机 Go 1.27.1 Linux amd64 下,编译出的短程序退出 2,stderr 以 panic: assignment to entry in nil map 开头。编译二进制与实际运行的原始双流在 evidence/07/nil-build.*、nil-write.*;具体栈地址和进程退出码不是 Go 语言规范给所有平台的 API 承诺。
这份隔离程序放在 testdata/,日常 go test -count=1 -v ./... 不会意外执行 panic;真正使用的索引通过 TestIndexTasks 验证可写、首项可查及缺失标记。nil map 可以用来表示尚未初始化或空集合,但不能把向其写入当作自动分配的 make。如果只写 position := positions[id],并不会 panic,却会把合法的第 0 项和缺失项混成同一个整数,这种缺陷比直接 panic 更难从成功输出察觉。
列表顺序要在输出层建立
Go 规范没有规定 map 的遍历顺序。--list-ids 需要可复跑的输出,于是 orderedTaskIDs 先 range positions 收集所有键,再用 sort.Slice 按映射中记录的提交位置排序。这里每个生成的 ID 对应不同的位置,因此无需依赖相等元素的稳定排序。实测 --batch 'Go|one two' --list-ids 按 task[0]、task[1] 两行输出、退出 0;TestIndexTasks 还使用 12 项,确认 task[2] 排在 task[10] 前面,没有直接对形如 task[10] 的字符串做字典序排序。
循环读取 map 得到某一次 task[0], task[1],不能据此推断下一次也一样;语言规范也没有保证每次都换顺序。当前输出按提交位置排序。证据是排序代码和带 12 项的断言,不是几次遍历偶然出现的先后关系。--lookup 与 --list-ids 同时提供时属于用法错误;它们只支持 --batch。原 --text 'Go 写 Go' 在本章再次实测输出 words=3 bytes=9 runes=7,默认批次输出不变;根包与 stats 包测试均退出 0。
练习:把 position, found := positions[id] 改为 if positions[id] == 0 { return notFound },会错判哪个 ID?答案:当前批次真实存在的 task[0] 值正是 0,它会被判成不存在;task[missing] 同样读出 0。改用两值索引判断 found,再用 position 读取 results,才能让「键是否存在」和「位置数值是多少」分别发挥作用。
本章只有当前进程中的顺序查询,没有重启后保留 ID 的能力,也没有并发安全性、数据库、HTTP 或发布验收。go1.27.1 的源码 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 取自 Go 镜像,官方 Gitiles 交叉核验仍未完成。下一篇封装任务记录并测试值复制和指针接收者,再决定索引应该指向怎样的对象。
参考资料
- Go 语言规范:Map types、Index expressions、range clause。本版工具链
doc/go_spec.html的对应小节及核对范围见writing-plans/go-language/research/07.md。 sort.Slice。排序比较当前批次的整数位置,不依赖 map 的遍历实现。






