Go 09:查询接口由谁需要的方法决定
08 把任务的 ID、结果和状态放进 TaskRecord。查询当前批次时,调用方实际需要几种方法?只要凭 ID 取到一份记录并辨认是否存在;创建批次、列出所有 ID 和更新状态不必进入同一个接口。先把接口定义为「存储系统的全部能力」,测试替身反而需要伪造一套写入协议。
同一累计工程在查询方附近声明 TaskLookup,只有 Lookup(id string) (TaskRecord, bool)。原 CLI 的 --lookup 'task[0]' 现在通过该接口调用;当前批次实体 BatchTasks 仍用 07 的 map 作为索引,不变成新工程、后台任务或持久化数据库。本章完整工程与 00–09 证据可下载,SHA256SUMS验证其内容。
独立复跑步骤列出本章正反例、源码校验及运行命令。
从一次查找收紧接口
describeTask(source TaskLookup, id string) 只问一件事:调用 source.Lookup(id),不存在就返回 "", false,存在则返回 "task[0] words=...", true。CLI 保持已有责任:若 found=false,stderr 输出 task not found: ...,应用退出 2;若找到,写一行到暂存 stdout 并退出 0。BatchTasks 的 IDs() 供 --list-ids 使用,但它不在 TaskLookup 的方法集合内:只需要查询的函数不能顺便要求所有替身都提供列表、构造和修改能力。
1 | |
实现方 BatchTasks 保存本次的 []TaskRecord 与 map[string]int,Lookup 用 map 的两值索引把缺失和真实的第 0 项分开;找到后返回记录值。07 的 indexTasks(records) 继续由记录 ID 建索引。这个实现暂时不承诺写入后的查找、重启恢复或多 goroutine 同时访问;接口足够小,并不自动解决存储生命周期。
lookup_test.go 定义只有一个值接收者 Lookup 的替身 lookupStub。同一 describeTask 使用真实 *BatchTasks 和替身分别返回同样的已找到描述,使用空替身返回 found=false。两个编译期断言要求 lookupStub{} 与 (*BatchTasks)(nil) 都实现 TaskLookup。(*BatchTasks)(nil) 这句只是类型检查,不调用 nil 指针上的方法;测试用的真实实现通过构造函数建立非 nil 的 map 与记录。go test -count=1 -v ./... 的两个包均退出 0,针对 TestTaskLookup 和前篇 TestTaskRecordCopyAndPointerReceiver 的单独测试也退出 0,命令和原始结果见 writing-plans/go-language/evidence/09/。
能调用指针方法,不等于值实现接口
BatchTasks.Lookup 的接收者是 *BatchTasks;lookupStub.Lookup 的接收者是 lookupStub。Go 1.27.1 规范中,定义类型 T 的方法集含接收者为 T 的方法,*T 的方法集还含接收者为 *T 的方法。因此值替身满足读接口;真实实现选用指针类型作为接口值,BatchTasks 的普通值并不满足这个接口要求。
08 曾经在可寻址的 record 变量上直接调用指针接收者方法。那是一次方法调用的自动取址规则;将值放进接口时检查的是值类型的方法集,不能靠调用语法推导接口实现。为避免把一次成功的正例写成猜测,本章 testdata/interface-value-error.go 另起一个最小编译程序:tasks 只有接收者为 *tasks 的 find,却写 var _ lookup = tasks{}。本机 go build 退出 1,stderr 明确指出 tasks does not implement lookup (method find has pointer receiver)。把检查对象改为 (*tasks)(nil) 才符合这条方法集规则;正例用当前工程的两个编译期断言实测通过。反例不参与正常主工程编译。
用编译二进制复测当前批次 --batch 'Go|one two' --lookup 'task[0]',stdout 是 task[0] words=1 bytes=2 runes=2,退出 0;查询 task[missing] 时 stdout 空、stderr 有 task not found:,退出 2;--list-ids 仍输出两个按位置排序的 ID。原 --text 'Go 写 Go' 输出 words=3 bytes=9 runes=7,退出 0。evidence/09/lookup.*、missing.*、list.*、original.* 记录实际命令、环境、原始双流与退出码。解包本章素材后,这些正反路径及编译反例也分别重跑;Hexo 页面验收单列,不代替这些 Go 检查。
练习:如果把 IDs() []string 也放进 TaskLookup,现有 lookupStub 会怎样?答案:它不再满足新接口,编译期断言失败。测试替身必须额外提供与 describeTask 完全无关的列表方法,接口在消费者这一侧已经变大。如果未来有函数确实需要按序列出 ID,可为那个消费方单独要求该能力;不需要为了一个尚不存在的用例提前修改查询契约。
本章没有通过接口隔离自动获得线程安全或重启恢复,当前 BatchTasks 仍只对一次顺序 CLI 调用负责。Go 1.27.1 源码 tag go1.27.1,镜像来源 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0,官方 Gitiles 交叉核验仍未完成。下一篇将检验一个错误接口值为何可能包含 nil 指针;在看到实际动态类型和动态值之前,不用 err != nil 推断具体存储形态。race、SQL、HTTP、其他平台和发布 NOT_RUN。
参考资料
- Go 语言规范:Interface types、Method sets。固定版本的本地规范行号、正反实现和边界记在
writing-plans/go-language/research/09.md。






