应使用 errors.join 而非手动拼接字符串来合并多个 error,因其保留原始错误类型、堆栈及可判断性,支持 errors.is/as 递归检查,且自动忽略 nil;但需注意避免 defer 中误用及确保子错误用 %w 包装以维持链式可识别性。

多个error需要同时返回时,用 errors.Join 而不是手动拼接字符串
Go 1.20 引入的 errors.Join 是处理多个 error 的标准方式。它不是简单把错误拼成字符串,而是构建一个可遍历、可判断、可展开的错误链。手动用 fmt.Errorf("a: %v; b: %v", errA, errB) 会丢失原始 error 类型和堆栈,后续无法用 errors.Is 或 errors.As 判断。
-
errors.Join返回的 error 实现了Unwrap方法,支持多层嵌套展开 - 传入
nil会被自动忽略,不用提前过滤 - 如果所有参数都是
nil,返回nil,符合 Go 错误语义 - 示例:
err := errors.Join(err1, err2, err3),之后可用errors.Is(err, someErr)检查任一子错误是否匹配
用 errors.Is 和 errors.As 判断 errors.Join 结果时,会递归检查所有子错误
这是 errors.Join 的核心价值:它让“组合错误”仍保持语义可识别性。比如你封装了一个批量操作函数,内部多个 goroutine 各自返回 error,用 Join 合并后,调用方仍能统一判断是否含某种特定错误类型。
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
-
errors.Is(err, os.ErrPermission)在err是errors.Join结果时,会逐层Unwrap直到找到匹配项或结束 -
errors.As(err, &target)同样递归查找,只要任一子 error 可转为target类型,就返回true - 注意:
errors.Unwrap只返回第一个子 error(按传入顺序),但Is/As是全量扫描
不要在 defer 中直接用 errors.Join 累加 error,容易漏掉 nil 判断或覆盖逻辑
常见反模式:在循环里反复 err = errors.Join(err, stepErr),尤其配合 defer 使用时,容易因初始 err 是 nil 导致第一次调用后变成非 nil,后续又意外覆盖。
- 更安全的做法是收集 error 切片,最后一次性
Join:errs = append(errs, stepErr),结尾用errors.Join(errs...) - 如果必须边执行边合并,记得判空:
if stepErr != nil { err = errors.Join(err, stepErr) } - 特别注意 defer 里的赋值:若函数签名返回
err error,且 defer 中修改err,要确保不会因多次执行 defer 而重复 Join 同一个 error
当需要区分错误来源或保留上下文时,errors.Join 不够用,得用自定义 error 包装
errors.Join 解决的是“有哪些错误”,但不解决“每个错误来自哪一步”。比如批量删除文件,你想知道 “file_a.txt: permission denied”、“file_b.txt: no such file”,这时单纯 Join 会丢失 key 信息。
- 推荐组合使用:
fmt.Errorf("delete %s: %w", filename, realErr)包装每个子错误,再Join这些带上下文的 error - 这样既保留结构化信息,又支持
errors.Is对底层错误(如os.ErrNotExist)做判断 - 避免把所有信息塞进一个 error 字符串——那会让
Is/As失效,也难以自动化解析
errors.Join 很轻量,但它的威力完全依赖你是否让每个子 error 本身是可识别的。最常被忽略的点是:忘了给中间 error 加 %w,导致整条链断掉。golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










