errors.Join 是 Go 1.20+ 官方多错误聚合工具,返回可被 errors.Is/As 识别的复合错误,自动过滤 nil,不拼接字符串而保留原始错误结构,不可用 == 比较,不替代 %w 包装。

errors.Join 是 Go 1.20+ 才有的原生错误合并工具
Go 1.20 之前没有标准库支持多错误聚合,开发者常靠自定义结构体或第三方包(如 pkg/errors)拼接。从 1.20 开始,errors.Join 成为官方推荐方式,它返回一个实现了 error 接口且内嵌多个错误的复合错误值,能被 errors.Is 和 errors.As 正确识别。
注意:它不拼接错误消息字符串,而是保留原始错误的完整结构和类型信息——这是和 fmt.Errorf("xxx: %w", err) 的本质区别。
调用 errors.Join 时传 nil 会被自动过滤
errors.Join 对 nil 参数是安全的,不会 panic,也不会将其纳入结果。这在动态收集错误的场景中很实用,比如循环中累积校验错误:
var errs []error
for _, item := range items {
if err := validate(item); err != nil {
errs = append(errs, err)
}
}
finalErr := errors.Join(errs...)
但如果直接写 errors.Join(err1, nil, err2),nil 会被静默丢弃,最终只合并非 nil 错误。这点容易被忽略,尤其在条件分支后手动拼参数时。
- 传入空切片(
errors.Join()或errors.Join([]error{}...))返回nil - 所有参数都是
nil→ 返回nil - 混合
nil和非nil→ 只合并非nil的
errors.Join 返回的错误不能用 == 比较,要用 errors.Is 判断子错误
因为 errors.Join 返回的是新构造的复合错误对象,即使内容完全一样,== 也会返回 false。判断某个底层错误是否存在于合并结果中,必须用 errors.Is:
errA := fmt.Errorf("db timeout")
errB := fmt.Errorf("cache miss")
joined := errors.Join(errA, errB)
fmt.Println(joined == errA) // false
fmt.Println(errors.Is(joined, errA)) // true
fmt.Println(errors.Is(joined, errB)) // true
同样,若需提取某类错误(比如 *os.PathError),也必须用 errors.As,不能靠类型断言原错误变量。
-
errors.Is会递归检查整个错误链,包括Join中的每一项 - 如果某子错误本身又是
Join结果,也会继续展开 - 性能上比单个错误略低,但对绝大多数业务场景无感
不要用 errors.Join 替代错误包装(%w)
errors.Join 和 fmt.Errorf("context: %w", err) 解决的是不同问题:%w 表示“因果关系”,强调上下文包裹;Join 表示“并列关系”,强调多个独立失败同时发生。
比如并发请求多个服务,全部失败了,应该用 Join;而 HTTP 请求失败后重试数据库,失败原因要保留 HTTP 错误,则该用 %w。
- 混用时注意顺序:先
%w包装再Join合并,比反过来更利于错误溯源 -
Join不改变原有错误的Unwrap()行为——它自己只返回第一个子错误(按参数顺序),其余需靠Is/As查找 - 日志打印时默认只显示第一个子错误的消息,想看全量需自行遍历或用第三方工具(如
github.com/hashicorp/go-multierror)
真正要注意的是:一旦用了 Join,就别再依赖 err.Error() 的字符串内容做逻辑判断——它只是简单拼接各子错误的 Error() 字符串,格式不稳定,也不包含类型信息。所有关键判断必须走 errors.Is 或 errors.As。

















