10 确定了查询成功必须返回真正的 nil 接口;缺失任务会返回非 nil 错误。接下来调用方还需要区分空文本、查不到 ID 与文件打不开:如果只根据 err.Error() 的字符串做分支,包装上一层 lookup "task[missing]" 就会改变调用方判断,OS 路径也可能进入 CLI 输出。错误类别应能跨包装层识别,诊断细节则留在内部错误链里。

沿用同一累计工程,本章为当前 CLI 的内部调用方声明 ErrInvalidInput、ErrTaskNotFound、ErrExecutionFailed 三种可用 errors.Is 判断的类别。这里的 package main 不是可供其他模块导入的公共错误库;CLI 对用户显示的消息另行决定。本篇完整累计工程与证据在只包含网站源码的环境中也能下载,附有SHA256SUMS。

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

哪一层知道错误类别

validateConfig(Config{}) 对非正的 MaxWords 包装非法输入类别;analyzeTask 对空白文本和超过字段上限的文本也返回 ErrInvalidInput。超限时保留已经算出的 Words=4 给 CLI 报 word count 4 exceeds max-words 3,调用者如果只关心类别,可用 errors.Is(err, ErrInvalidInput),不必拆错误文字。原 CLI 的参数解析与合法统计仍先于后续索引操作;第二条批次输入非法时不输出第一条的成功结果。

lookupRecord 发现 ID 不存在时创建非 nil 的 *missingTaskError,其 Unwrap() 返回 ErrTaskNotFound,再通过 fmt.Errorf("lookup %q: %w", ...) 添加一层上下文。上层收到的具体值是包装后的错误,直接断言 err.(*missingTaskError) 不再成立;errors.Is(err, ErrTaskNotFound) 判断类别,errors.As(err, &failure) 找到深处的非 nil 具体错误及其 ID。CLI 在 errors.Is 判断为未找到后,仍只打印 task not found: task[missing]。包装中 lookup "..." 的文字没有成为客户端稳定协议。调用者也无需借助指针是否 nil 来判断第 0 项是否存在,那个判断发生在 07 的 map 两值查询中。

文件输入是本章具体的执行失败路径。readInputFile 仍负责打开、读取和关闭;readTaskFile 在失败时使用两个 %w,一边包 ErrExecutionFailed,另一边包真实的 OS 错误。测试对不存在的临时路径断言:errors.Is(err, ErrExecutionFailed) 为真,errors.Is(err, os.ErrNotExist) 也为真,errors.As 可取到 *os.PathError。这使内部诊断仍有文件、操作和原因可查,却不要求 CLI 把绝对路径原样写给用户。程序的 stderr 只显示 read file: task execution failed,stdout 空,应用退出 2。具体路径在原始实验命令和内部测试结果中,不在这条对外文本里。

1
2
3
错误出现处:validateConfig / analyzeTask / lookupRecord / readTaskFile
-> 内部调用者:errors.Is 匹配分类,errors.As 定位底层类型
-> 当前 CLI:按分类返回退出码与有限的诊断文本

内部有两个事实需要分开验证。fmt.Errorf 多个 %w 在当前固定 Go 1.27.1 的公开文档中有 Unwrap() []error 的约定,可沿两个分支检查;errors.As 能找到底层 PathError 不表示它的完整 Error() 字符串应该作为 HTTP/CLI 协议。将来需要把错误映射到 HTTP 状态码时,处理器可以读分类,不应复制本次 Linux 的绝对路径、系统错误后缀或 fmt 包装文案。

正反路径的实际结果

在 Linux amd64、Go 1.27.1、GOTOOLCHAIN=local GOWORK=off GOMAXPROCS=2 CGO_ENABLED=0 下,go test -count=1 -v ./... 的根包与 stats 包退出 0,TestErrorKinds 对分类、底层 PathError 和 CLI 不泄露路径逐一断言通过。编译后二进制测试的几条退出 2 的路径各有单独双流:空文本报 usage: taskstats: text must not be empty;四个词超过默认上限时报 word count 4 exceeds max-words 3;零上限报 max-words must be positive: invalid input;未知 ID 报 task not found: task[missing];缺失文件报 read file: task execution failed。这些路径 stdout 均为空。合法 --text 'Go 写 Go' 仍输出 words=3 bytes=9 runes=7、退出 0;已存在的 task[0] 查询输出不变。所有运行命令、环境、原始 stdout/stderr 与退出码在 writing-plans/go-language/evidence/11/,打包后在素材 unpack-* 中从独立解包目录再运行一次。

练习:包装后的缺失错误 err 上,err.(*missingTaskError) 失败,为什么 errors.As(err, &failure) 可以成功?答案:类型断言只检查此刻接口里的顶层动态类型,顶层是 fmt.Errorf 构造的包装值;errors.As 按标准库错误树遍历 Unwrap,才看到下面的 *missingTaskError。需要业务分类时优先用 errors.Is(err, ErrTaskNotFound);需要内部附加信息时再用 errors.As。两者不能用错误字符串相等代替。

本章的执行失败只来自文件读路径,没有 SQL、worker 或 HTTP;文件不存在既不意味着后台任务已经失败,也不意味着数据库做过提交。其他平台、race 和网站发布均 NOT_RUN。Go 源码 tag go1.27.1、镜像 SHA 862c888e612ac346c7c4d99c9392bdfd265f33b0 已登记,官方 Gitiles 交叉核验未完成。下一篇用可控 panic 验证清理和 recover 的 goroutine 边界,不用 panic 代替普通输入错误。

参考资料

  • fmt.Errorf、errors.Is、errors.As。Go 1.27.1 本地 go doc 的原始结果与固定 tag 文件 src/fmt/errors.go:16 的核对方式见 writing-plans/go-language/research/11.md。