Go errors.Join 怎么用?多条错误合并后还能用 errors.Is 判断吗
来源:17golang原创
时间:2026-07-16 10:09:59 431浏览 收藏
日常写清理逻辑,要同时关文件、提交事务、释放临时文件夹,哪一步失败都不该把前面的错误悄咪咪吃掉。Go 1.20 之后可以用 errors.Join 把多条错误合并到一起;合并后的结果照样能交给 errors.Is 和 errors.As 做判断。它特别适合需要完整带出多个失败点的收尾场景,但不能用来掩盖哪一步才是核心的主失败。
errors.Join 会自动丢弃所有 nil 错误;如果传入的全部是 nil,返回结果也直接是 nil。只要入参里有非 nil 错误,返回值就会保留完整的多条错误链,所以 errors.Is 依然可以用来判断某个哨兵错误,errors.As 也能正常提取对应类型的错误。实际用的时候先理清主错误是什么,再确定哪些清理类的附加错误要一并返回。
核心要点
errors.Join最适配资源收尾、批量处理、并行任务结果汇总的场景,不用自己手写字符串拼接。- 合并后输出的错误文本默认按换行分隔,面向最终用户的提示内容还是要调用方自己转成清晰、语义稳定的业务提示。
errors.Is会顺着多错误的包装链逐层判断,原来定义的哨兵错误不会因为合并就失效。- 已经遇到明确的主失败时,要优先保留主失败的语义,别让一大堆次要错误把真正的故障原因淹掉。
合并多条错误时,到底保留了什么
标准库的 errors 包文档说明,Join 支持传入多个错误参数,自动忽略其中的 nil 值;如果所有入参都是 nil,返回结果也为 nil。只要存在非 nil 的错误值,返回的错误对象就会包装所有传入的非 nil 错误,并且以 Unwrap() []error 的形式暴露多条错误链路。也就是说它不是先把所有错误转成普通字符串再拼在一起。
这一点非常关键。过去大家常用的 fmt.Errorf("close failed: %v; remove failed: %v", a, b) 虽然看起来信息全,但是后续代码就彻底失去了结构化判断错误的机会。调用方只能靠匹配错误字符串做判断,日志系统也很难按错误类型做归类。用 errors.Join 之后,错误的可读信息正常打印,同时错误的类型、哨兵错误的标记也都完整保留了下来。
| 场景 | 是否适合 Join | 原因 |
|---|---|---|
| 批量关闭多个资源 | 适合 | 每个关闭动作都可能出问题,漏掉会影响后续故障排查 |
| 并行校验多个独立输入 | 适合 | 用户一次就能看到全部的校验不通过项 |
| 数据库写入失败后清理临时文件 | 谨慎使用 | 写入失败通常是核心原因,清理失败只适合作为附加信息 |
| 同一操作的多次重试 | 通常不适合 | 更需要最后一次的错误和重试上下文,全部堆在一起信息会非常杂乱 |

先别急着合并:分清主失败和收尾失败
很多人刚接触 errors.Join 的时候,会顺手把所有遇到的错误都塞进去。这种写法在资源收尾阶段用着很方便,但放到业务主路径上反而会把故障的判断逻辑搞模糊。比如订单写入数据库失败,之后删除临时文件也跟着失败;对上游调用方来说,写库失败直接决定了整个请求的结果,删除失败只需要单独记录日志、后续走补偿逻辑就行,不该和主失败放在同一优先级。
一个很实用的判断标准:如果多条错误代表的是互相独立、并行发生的工作,合并起来逻辑就很通顺;如果其中某一条错误直接决定了整个业务操作的最终成败,先用包装错误把核心语义保留好,其余附带的错误可以单独记录到日志、埋点指标或者补偿队列里。我们的目标不是返回的错误对象越多越好,而是上层调用方拿到错误之后能执行正确的处理逻辑。
资源收尾的常见写法
下面的例子模拟一次导出任务。任务主体可能已经执行完成,但文件关闭、临时文件夹清理这两个动作的错误都不能被直接吞掉。把所有收尾阶段的错误累积起来,最后确认确实有错误的时候再统一返回:
func cleanup(tmp string, file *os.File) error {
var errs []error
if file != nil {
if err := file.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭导出文件: %w", err))
}
}
if err := os.RemoveAll(tmp); err != nil {
errs = append(errs, fmt.Errorf("清理临时目录: %w", err))
}
return errors.Join(errs...)
}
这段代码有两个很容易被忽略的细节。第一,存放错误的切片为空的时候,errors.Join(errs...) 会直接返回 nil,不需要额外加判断分支。第二,给每条错误补充清晰的上下文之后再做合并,排查日志的时候一眼就能知道是关闭文件失败还是清理目录失败;同时原本的 %w 会被保留,底层的哨兵错误、自定义错误类型还是能被正确识别。
errors.Is 和 errors.As 在合并后还能用吗
完全可以。Go 1.20 的发布说明里明确提到,多错误的包装机制已经被纳入 errors.Is 和 errors.As 的检查逻辑里。实际开发中可以继续用哨兵错误判断要不要触发重试、要不要给用户提示权限不足,也可以直接提取自定义的错误类型做针对性处理。
var ErrQuota = errors.New("quota exceeded")
func classify(err error) string {
if errors.Is(err, ErrQuota) {
return "quota"
}
var pathErr *fs.PathError
if errors.As(err, &pathErr) {
return "path"
}
return "other"
}
如果 err 是多条错误合并出来的结果,只要其中任意一条子错误和 ErrQuota 匹配,上面的判断结果就会返回真。不要把这个特性误解为「第一条错误优先」:合并后的错误代表的是多条错误同时发生,匹配逻辑会沿着每一条被包装的错误链路一直往下找。

不要用错误文本承担业务协议
Join 生成的默认文本会把各个错误的消息用换行符连起来。这种格式打印到开发日志里很直观,直接放到接口响应里就不合适了:很可能泄露存储路径、下游依赖的内部信息,前端也没法做稳定的展示。对外暴露的 HTTP、RPC 接口或者消息消费端,还是应该返回固定的业务码和面向用户的提示语,完整的合并错误内容只留在服务端本地日志和链路追踪系统里就好。
同样的,不要靠判断错误文本里有没有某个子串来决定要不要重试。对于逻辑可控的分支,直接用 err.Error() 的包含关系判断哨兵错误,需要拿自定义错误里的字段时,用 errors.Is 来提取。错误文本是给开发人员排查问题看的,不能当做程序之间传递信息的不稳定协议。
几个容易踩的边界
- 只支持 Go 1.20 及以上版本。低版本标准库没有自带
errors.Join,升级受限制的时候要提前评估兼容方案,别在公共依赖库里无意间抬高了要求的最低 Go 版本。 - 不要无限制累积错误。循环处理十万条输入的时候,把每一条失败都合并返回会导致内存、日志、响应体积完全失控。可以设置一个累积上限,保留少量错误样本,把总失败数写到监控指标里。
- 并行任务收集错误要做同步。多个 goroutine 不能同时往同一个错误切片里 append 操作,要么用 channel 汇总结果,要么加锁在受保护的临界区里写入。
- 与多个 %w 的关系。Go 1.20 之后
fmt.Errorf也支持同时传入多个%w来包装多条错误。需要顺手给合并后的错误加一句总说明的时候用它很方便;纯碎汇总数量不固定的一组错误时,errors.Join的写法会更直白。
上线前的短清单
- 确认项目的 Go 版本至少为 1.20,CI 构建环境和生产环境的镜像版本保持一致。
- 梳理所有可能并行失败的收尾动作,给每条错误补充简短的业务上下文说明。
- 针对用到的核心哨兵错误写一个
errors.Is单测,确认合并之后的判断分支逻辑仍然符合预期。 - 针对自定义的错误类型写一个
errors.As单测,确认错误字段提取的功能没有失效。 - 检查对外接口的响应逻辑,不会直接把合并后的原始错误文本暴露给最终用户。
常见问题
errors.Join 只有一条错误时会怎样?
它仍然会返回一个包装了这条错误的结果。调用方不需要为「只有一条错误」的场景额外写分支,照常使用 errors.Is 和 errors.As 处理就可以。
errors.Join 能合并 nil 吗?
可以,传入的 nil 会被自动忽略;如果所有参数都是 nil,返回结果就是 nil。这个特性刚好适合把条件性生成的清理类错误直接放到同一个收集流程里,不用额外判空。
多个错误的展示顺序可靠吗?
展示顺序完全按你传入 errors.Join 的顺序决定。如果需要日志输出的顺序保持稳定,可以先按资源编号或者任务编号把错误排好序,再执行合并,后续排查的时候更容易比对。
所有并发任务失败都应该合并吗?
不一定。互相独立的校验任务很适合合并;触发第一个失败就必须立刻停止的任务,更适合取消剩下的未完成工作,优先保留第一个失败的错误和完整上下文。
fmt.Errorf 的多个 %w 能替代 errors.Join 吗?
两者都能生成支持多错误包装的错误对象。错误数量固定只有两三条,还需要补充一句总说明的时候可以用 fmt.Errorf;错误数量是动态变化、需要后续统一收集汇总的场景,errors.Join 的写法会更方便。
处理多错误的目标不是把日志写得更长,而是在完整保留所有并列失败信息的同时,不给调用方传递模糊的业务结论。先把主失败界定清楚,只在确实是多错误并列的场景下使用 errors.Join,写出来的代码排查起来更顺畅,后续迭代也更容易维护。
-
278 收藏
-
483 收藏
-
291 收藏
-
195 收藏
-
412 收藏
-
Golang · Go教程 | 37分钟前 | golang · Timer · 并发编程 · time.After · 性能排查 · time.After go timer Go 1.23 NewTimer Timer.Reset Timer.Stop403 收藏
-
Golang · Go教程 | 1小时前 | 并发 · 错误处理 · go · Context · 排错 · Go 错误链 context.WithCancelCause context.Cause 取消原因366 收藏
-
Golang · Go教程 | 2小时前 | 并发 · WaitGroup · go · worker pool · 服务停机 · Go channel WaitGroup 优雅停机 worker pool 任务排空207 收藏
-
294 收藏
-
Golang · Go教程 | 3小时前 | 切片 · Slices · Go教程 · Go 1.21 · 切片删除 Go教程 Go slices.Delete slices.DeleteFunc Go 1.21463 收藏
-
Golang · Go教程 | 5小时前 | HTTP · Go教程 · 服务治理 · Shutdown · SIGTERM · SIGTERM Go 优雅关闭 http.Server Shutdown Go HTTP 服务 连接收尾 服务下线316 收藏
-
395 收藏
-
255 收藏
-
380 收藏
-
234 收藏
-
251 收藏
-
334 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习