09 的 TaskLookup 用 Lookup(id) 返回记录和是否存在的布尔值。查询方往往还要把「无此任务」沿调用栈返回给 CLI:如果函数声明返回 error,在成功路径先留一个 *missingTaskError 类型的 nil 指针,直接把它作为 error 返回,会不会等价于 return nil?不会。此时 err != nil 为真,即使内部那个指针为 nil。

同一累计工程在查询路径加入 lookupRecord(source, id) (TaskRecord, error)。查到任务时返回记录和字面 nil;未查到时返回非 nil 的 *missingTaskError,describeTask 把错误交给 CLI。旧的 --lookup 'task[missing]' 仍输出 task not found: task[missing] 到 stderr 并退出 2,成功查询与其他入口输出不变。真实反例、测试和完整工程见本章素材包与SHA256SUMS。

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

比较的是接口值,不只是指针

Go 规范允许接口值同时携带具体的动态类型与动态值。把 var failure *missingError(值为 nil)赋给 error 后,接口携带类型 *missingError 和值 nil;整个接口不等于 nil。类型断言 err.(*missingError) 可以成功,取出的具体指针依然是 nil。成功路径直接返回字面 nil 时,接口本身为 nil,两值类型断言的 ok=false。

隔离短程序 testdata/typed-nil.go 实现 incorrectSuccess() error 和 correctSuccess() error,分别返回上述两种情况。Linux amd64、Go 1.27.1 上,go run ./testdata/typed-nil.go 退出 0,实际 stdout 为:

1
2
bad: error-is-nil=false type-ok=true pointer-is-nil=true
good: error-is-nil=true type-ok=false pointer-is-nil=true

两行都打印 pointer-is-nil=true,但第二行的指针是失败断言时得到的零值,不能将其解释为接口内部也存了一个 nil 指针。第一行显示了本章要避免的逻辑反例:声称「成功」的函数返回了一个被调用方视作错误的非 nil 接口。短程序的进程退出 0 只表示程序按预定方式展示了差异,不表示 bad 路径的业务行为正确。源码和实测双流、退出码在 writing-plans/go-language/evidence/10/typed-nil.*。

实际查询怎样返回没有任务

1
2
3
4
5
6
7
func lookupRecord(source TaskLookup, id string) (TaskRecord, error) {
record, found := source.Lookup(id)
if !found {
return TaskRecord{}, &missingTaskError{id: id}
}
return record, nil
}

真实代码把 map 的两值查询结果沿 07 的约定分开处理。成功路径直接返回 nil,不让一个已声明但尚未填充的 *missingTaskError 经隐式转换逃进接口;失败路径则创建有 ID 的非 nil 指针。TestLookupErrorInterface 验证:成功结果 err == nil 且对 *missingTaskError 的两值断言 ok=false;缺失结果断言 ok=true、指针非 nil 且 ID 与查询目标一致。测试还显式构造 var broken error = (*missingTaskError)(nil),确认接口非 nil、断言成功、具体指针仍 nil。这些是三种不同形状,不能仅用 err != nil 判别后两者的具体内容。

describeTask 把实际 lookupRecord 的错误原样返回,CLI 只在 err != nil 时写出 err.Error() 对应的描述。本章真实路径创建的 missingTaskError 指针非 nil;隔离 bad 程序故意只做比较与断言,没有去调用 nil 指针接收者的方法。因此不能从 bad 的 err != nil 推出 Error() 必然安全。未来若调用方要稳定地辨认「未找到」,不能直接把这里尚未公开的具体错误类型当作固定 API;下一篇再冻结错误识别契约。

本次编译主二进制后,查询 task[0] 输出 task[0] words=1 bytes=2 runes=2、stderr 空、退出 0;查询 task[missing] stdout 空、stderr task not found: task[missing]、退出 2;原 --text 'Go 写 Go' 输出 words=3 bytes=9 runes=7、退出 0。go test -count=1 -v ./... 在根包和 stats 包均退出 0;单独的接口错误测试也退出 0。这些是 evidence/10/ 保存的当前环境实际命令、双流和退出码,不是对未运行版本的推断。素材包经校验并独立解包后,主测试、查询及两种接口值短程序都再次运行。

练习:var specific *missingTaskError; var err error = specific,此时 err == nil 和 specific == nil 各返回什么?答案:前者为 false,后者为 true。value, ok := err.(*missingTaskError) 则得到 ok=true、value==nil。如果需要表示没有错误,返回没有动态类型的字面 nil;如果需要表示未找到,构造一个真正非 nil 的错误值,或在读取数据的那一层继续保留 found 布尔值。

当前 ID 只在一次顺序批次中可查,尚无 SQL、HTTP、并发取消或后台任务。其他平台、race 和发布均 NOT_RUN。Go 源码 tag go1.27.1,镜像查询 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0,官方 Gitiles 交叉核验尚未完成。下一篇在已观察的成功/缺失上建立调用层可判断的错误契约。

参考资料