Go 1.20+ 推荐用 errors.Join 合并多个错误,它按参数顺序拼接、忽略 nil、支持 errors.Is/As;需显式判空,fmt.Printf("%+v", err) 查看完整嵌套;自定义错误应实现 *T.Unwrap() error;multierr 更适合动态聚合;API 响应须结构化,勿直接暴露复合错误。

Go 1.20+ 使用 errors.Join 合并多个错误
当函数内部触发多个独立错误(比如并发操作中多个 goroutine 失败),又不想只返回第一个,errors.Join 是最直接的内置方案。它把多个 error 合成一个可展开的复合错误,且支持 errors.Is 和 errors.As。
常见错误是误以为 errors.Join 会“去重”或自动排序——它只是按参数顺序拼接,且不检查 nil;传入 nil 会被忽略,但不会报错,容易漏掉本应关注的错误。
- 必须显式检查每个子错误是否为 nil,再决定是否加入:
errors.Join(err1, err2, err3)中若err2 == nil,它不会出现在最终错误中 - 适合场景:批量 I/O、并行 HTTP 请求、多数据库写入等“尽力而为”型逻辑
- 注意:
errors.Join返回的错误在fmt.Println下默认只显示摘要,需用fmt.Printf("%+v", err)才能看到全部嵌套栈
自定义错误类型实现 Unwrap 支持链式错误
如果需要携带上下文(如操作名、ID、重试次数)或控制错误展示格式,不能只靠 errors.Join。此时应定义结构体并实现 Unwrap() error 方法,让错误可被标准库递归解析。
典型陷阱是忘记返回指针类型错误——Unwrap 签名要求返回 error 接口,若方法值接收者返回的是值拷贝,可能导致循环引用或无法正确解包。
立即学习“go语言免费学习笔记(深入)”;
- 推荐用指针接收者:
func (e *MultiOpError) Unwrap() error - 避免在
Unwrap中做复杂计算或阻塞操作,它可能被日志、监控等中间件高频调用 - 若需保留多个底层错误(不止一个),可返回
errors.Join结果作为Unwrap返回值,形成混合结构
使用 multierr 库替代手写聚合逻辑
multierr(由 Go 团队成员维护)比 errors.Join 更灵活:支持追加、过滤、延迟合并,并提供 multierr.Append 这类更符合直觉的 API。尤其适合错误收集发生在循环或条件分支中时。
它默认忽略 nil 错误,这点和 errors.Join 一致,但关键区别在于:它允许后续追加错误(multierr.Append(err, newErr)),而 errors.Join 每次都是全新构造。
- 安装:
go get go.uber.org/multierr - 不要在 defer 中无条件调用
multierr.Append,因为 defer 的执行顺序是后进先出,可能导致错误顺序与实际发生顺序相反 - 若需保留第一个非-nil 错误作为主错误(例如用于 HTTP 状态码映射),可用
multierr.First提取
避免在 HTTP handler 中直接返回 errors.Join 结果
Web 服务中,把 errors.Join 得到的复合错误直接塞进 http.Error 或 JSON 响应,会导致前端看到一长串难以解析的文本。错误信息需要结构化,而不是仅靠字符串拼接。
真正该暴露给客户端的,是业务语义明确的状态码 + 简洁 message + 可选 code 字段;原始复合错误应只用于服务端日志和追踪。
- 建议做法:用
errors.As或errors.Is提取关键错误类型(如*os.PathError、*sql.ErrNoRows),映射为预定义的 API 错误码 - 记录日志时再用
fmt.Sprintf("%+v", err)输出完整错误链,确保可观测性 - 不要把
errors.Join结果直接 JSON 编码——它的结构不是稳定 API,字段随时可能变
多重错误的本质不是“怎么合并”,而是“哪些错误值得暴露、以什么粒度暴露”。过度合并会丢失上下文,完全不合并又难诊断;关键是根据调用方角色(人 or 机器)、错误来源(系统级 or 业务级)、传播路径(日志/监控/API)来分层处理。










