Go 1.20+ 应优先使用 errors.Join 合并错误,它轻量、稳定、自动过滤 nil;multierr 仅适用于旧版本,且需区分 Append(累加)与 Combine(扁平化),避免嵌套过深导致 errors.Is 失效。

Go 1.20+ 直接用 errors.Join,别急着引入 multierr
Go 标准库从 1.20 开始原生支持错误合并,errors.Join 就是为此设计的。它比第三方 multierr 更轻、更稳、无额外依赖——除非你卡在旧版本(如 1.19 及以下),否则没必要引入新包。
常见误用:有人看到 multierr.Append 能“累积追加”,就以为 errors.Join 也得反复调用。其实 errors.Join 接收可变参数,一次就能合并任意多个错误:
err := errors.Join(err1, err2, err3, io.EOF)
注意:errors.Join 会自动过滤 nil,传入 nil 不报错也不影响结果;但若所有参数都是 nil,返回仍是 nil,不是空错误。
multierr.Append 和 multierr.Combine 到底该用哪个?
multierr.Append 是“累加式”合并,适合循环中逐个收集错误(比如遍历多个 goroutine 的返回 err);multierr.Combine 是“一次性”合并切片,语义更明确、性能略好(少一次切片扩容)。
立即学习“go语言免费学习笔记(深入)”;
- 用
multierr.Append:需要边执行边攒错误,且不确定最终有多少个 - 用
multierr.Combine:已有一组错误切片(如[]error),直接合并 - 二者都返回非
nil错误时,errors.Is和errors.As都能正常穿透到子错误
示例:
var errs []error
for _, f := range files {
if err := os.Remove(f); err != nil {
errs = append(errs, err)
}
}
finalErr := multierr.Combine(errs) // 比多次 Append 更干净
嵌套错误太多导致 errors.Is 失效?检查是否用了 multierr.Append 过度
multierr.Append 默认会把新错误“嵌套”进已有错误结构,连续调用多次可能形成深层嵌套(比如 Append(Append(err1, err2), err3)),而 errors.Is 默认只展开一层。这会导致 errors.Is(finalErr, fs.ErrNotExist) 返回 false,即使其中一个子错误确实是 fs.ErrNotExist。
解决办法:
- 优先用
multierr.Combine或errors.Join,它们扁平化合并,不嵌套 - 如果必须用
multierr.Append,改用multierr.AppendInto(v1.9+)或显式调用multierr.Flatten - 调试时可用
fmt.Printf("%+v", err)查看实际嵌套结构
生产环境慎用 multierr 的字符串拼接模式
multierr 默认把错误转成字符串再拼(如 "failed to X: failed to Y: ..." ),这对日志友好,但丢失了原始错误类型和字段。一旦下游用 errors.As 提取自定义错误(比如 *os.PathError),就会失败。
真实场景踩坑点:
- HTTP handler 中合并多个校验错误后返回,前端只收到模糊文本,无法做差异化提示
- 重试逻辑依赖
errors.Is(err, context.DeadlineExceeded),但被multierr包裹后判断失效
建议:只在最终日志或用户可见错误中做字符串聚合;中间传递、判断、重试等环节,始终用 errors.Join 或 multierr.Combine 保持错误可检出性。
复杂点在于:错误合并不是“越全越好”,而是要分清“给机器读”和“给人读”的边界。很多人一上来就无脑 Append 所有 err,结果调试时发现关键错误类型全丢了。










