errors.Join 聚合多个清理错误的返回方案
在一个函数里关闭文件、响应体、事务或临时资源时,清理动作往往不止一个。若每次都覆盖前一个错误,调用方最后只能看到最后一次关闭失败,前面的原因就丢了。Go 1.20 加入的 errors.Join 可以把多个非 nil 错误合并成一个返回值,并继续支持 errors.Is 和 errors.As 检查。
实用做法是:先把清理阶段产生的非 nil 错误收集到切片,再用errors.Join(errs...)返回。所有元素都是 nil 时它返回 nil;存在错误时返回一个实现Unwrap() []error的聚合错误。
官方资料:https://pkg.go.dev/errors#Join
为什么清理错误不能只保留最后一个
常见的旧写法是让多个 defer 依次给命名返回值赋值。由于 defer 按后进先出执行,最后一次赋值会覆盖前面的错误。这个问题在“主流程成功、清理阶段失败”时尤其容易被忽略:函数表面上返回了错误,但调用方无法知道还有哪些资源没有正确关闭。

聚合的重点不是把错误文本拼成一段日志,而是让返回值保留错误树。这样既能让日志看到多个原因,又能让上层继续按具体错误类型做判断。
先收集清理阶段的非 nil 错误
可以把资源关闭逻辑放进一个小函数,由它负责收集错误。下面的示例模拟文件、响应体和事务三个清理动作;实际项目中把三个变量替换成真实资源即可。
package main
import (
"errors"
"fmt"
)
type closeResource func() error
func closeAll(resources ...closeResource) error {
var cleanupErrs []error
for _, close := range resources {
if err := close(); err != nil {
// 只保存失败的清理动作,成功关闭不应进入返回值。
cleanupErrs = append(cleanupErrs, err)
}
}
// Join 会再次忽略 nil;切片为空时返回 nil。
return errors.Join(cleanupErrs...)
}
func main() {
err := closeAll(
func() error {
// 模拟文件关闭失败。
return errors.New("file close: flush failed")
},
func() error {
// 模拟响应体已经正常关闭。
return nil
},
func() error {
// 模拟事务回滚失败。
return errors.New("tx rollback: connection reset")
},
)
fmt.Println(err)
}
这个函数有三个值得保留的细节:
- 只在关闭动作返回非 nil 时追加,避免把“成功”伪装成错误。
- 按资源关闭的调用顺序收集错误,输出顺序更容易和清理代码对应。
- 统一由
errors.Join处理空切片和多个成员,调用方仍然只接收一个error。
用 errors.Join 组合主流程错误与清理错误
真实业务函数通常还有一个主流程错误。例如写入失败后需要回滚,回滚又可能失败。此时不要让清理错误覆盖主流程错误,而是把两者一起加入聚合列表。
func process() (err error) {
file, err := openFile()
if err != nil {
// 文件都没有打开成功,不需要进入清理阶段。
return err
}
defer func() {
// 关闭文件失败时,把它和主流程错误一起返回。
err = errors.Join(err, file.Close())
}()
if err = writeData(file); err != nil {
// 命名返回值保留写入错误,defer 会在返回前追加关闭结果。
return err
}
return nil
}
这里的关键是 errors.Join(nil, closeErr) 会得到关闭错误,而 errors.Join(mainErr, nil) 会保留主流程错误。两个都为 nil 时最终仍是 nil,因此不需要为“是否只有一个错误”再写一层分支。
如果清理动作不止一个,可以在 defer 中先调用 closeAll,再把结果与命名返回值合并:
defer func() {
cleanupErr := closeAll(file.Close, response.Body.Close, tx.Rollback)
// 主流程和清理阶段都可能失败,统一交给 Join 保留两边的信息。
err = errors.Join(err, cleanupErr)
}()
聚合后仍然可以用 errors.Is 和 errors.As
errors.Join 返回的错误不是只能打印的字符串。官方实现让非 nil 结果提供 Unwrap() []error,因此 errors.Is 可以在聚合结果的成员中查找哨兵错误,errors.As 可以提取某一种具体错误类型。
var (
errFileClosed = errors.New("file close failed")
errRollback = errors.New("rollback failed")
)
joined := errors.Join(errFileClosed, errRollback)
if errors.Is(joined, errRollback) {
// 聚合错误中包含回滚失败,可以触发补偿或告警。
fmt.Println("rollback needs attention")
}
type timeoutError struct{}
func (timeoutError) Error() string { return "cleanup timeout" }
var timeout timeoutError
joined = errors.Join(joined, timeoutError{})
if errors.As(joined, &timeout) {
// As 能从错误树中取出匹配的具体类型。
fmt.Println("cleanup timed out")
}
需要读取全部成员时,可以通过接口断言取得 Unwrap() []error。这适合记录结构化清理结果,但不要依赖标准库内部的具体类型名称;只依赖这个公开的解包方法即可。
type multiUnwrapper interface {
Unwrap() []error
}
if multi, ok := joined.(multiUnwrapper); ok {
for _, item := range multi.Unwrap() {
// 逐个记录成员,保留原始 error 供后续判断。
fmt.Println(item)
}
}
和“只返回最后一个错误”的方案有什么区别

| 处理方式 | 调用方能看到什么 | 适用情况 |
|---|---|---|
| 每次覆盖返回值 | 通常只剩最后一次清理失败 | 前序清理错误确实不需要保留时 |
| 手工拼接字符串 | 文本完整,但失去类型检查 | 仅用于展示日志,不作为程序错误返回值 |
| errors.Join | 多个错误文本与错误树都保留 | 多个清理动作都影响诊断或补偿时 |
errors.Join 也不是所有场景都该使用。如果错误之间存在明确的因果链,例如“读取配置失败,因为文件不存在”,应该使用 %w 包装来表达一条链;只有多个相对独立、需要同时呈现的失败,才适合聚合。另一个边界是错误数量:批量处理数万条记录时,不应无上限地把每一条原始错误都放入内存,应该在业务层先做数量限制或汇总。
落地时记住这三个判断
- 清理动作相互独立且可能同时失败时,先收集非 nil 错误,再调用
errors.Join。 - 主流程已经有错误时,不要用清理错误覆盖它,用
errors.Join(mainErr, cleanupErr)保留两类原因。 - 需要判断具体原因时使用
errors.Is或errors.As,不要解析Error()返回的换行文本。
常见问题
errors.Join 传入 nil 会不会报错?
不会。nil 会被丢弃;当所有参数都是 nil 时,返回值就是 nil。
errors.Join 的输出为什么有多行?
多个成员的错误文本会按参数顺序用换行连接。这个格式适合日志展示,但程序判断仍应使用 errors.Is 或 errors.As。
只有一个错误时还需要 Join 吗?
可以统一调用。单个非 nil 成员会保留其错误文本和可检查性,统一入口能减少清理代码中的条件分支。
errors.Join 能替代 fmt.Errorf("%w") 吗?
不能完全替代。%w 表达一条带上下文的错误链,Join 表达多个并列成员;根据错误之间的关系选择对应方式。
参考资料:https://pkg.go.dev/errors#Join、https://go.dev/src/errors/join.go、https://go.dev/doc/go1.20
slog.Group 将结构化日志按业务域分组的方法
- 上一篇
- slog.Group 将结构化日志按业务域分组的方法
- 下一篇
- 暂无
-
- Golang · Go教程 | 18分钟前 | Go http.Transport http.Client TLS握手超时 TLSHandshakeTimeout
- http.Transport 控制 TLS 握手超时的参数对比
- 373浏览 收藏
-
- Golang · Go教程 | 22分钟前 | go · https · TLS · tls.Config ServerName RootCAs Go HTTPS
- tls.Config 设置根证书池与服务器名称的配置方法
- 429浏览 收藏
-
- Golang · Go教程 | 25分钟前 | Go教程 · net/http · HTTP客户端 · 重试 · Go 请求重试 请求体 http.Client Request.GetBody
- http.Client 重试可重放请求体的设计要点
- 226浏览 收藏
-
- Golang · Go教程 | 26分钟前 | Go Cookiejar http.Client 会话Cookie http.CookieJar
- http.CookieJar 管理跨请求会话 Cookie 的实现方式
- 419浏览 收藏
-
- Golang · Go教程 | 29分钟前 | HTTP · go · httputil.ReverseProxy Go反向代理 Host请求头
- httputil.ReverseProxy 转发原始 Host 与请求头的配置方法
- 251浏览 收藏
-
- Golang · Go教程 | 30分钟前 |
- http.ResponseController 刷新长连接响应的使用边界
- 395浏览 收藏
-
- Golang · Go教程 | 33分钟前 |
- http.Server 优雅停机并等待连接收尾的实现方式
- 489浏览 收藏
-
- Golang · Go教程 | 35分钟前 | Go url.Values ParseQuery 重复查询参数 参数保序
- url.Values 读取重复查询参数的保序处理方案
- 279浏览 收藏
-
- Golang · Go教程 | 35分钟前 | 标准库 · url · Go教程 · 接口设计 · Go PathEscape URL转义 net/url.JoinPath 接口路径
- net/url.JoinPath 组合接口路径并保留转义的实践示例
- 298浏览 收藏
-
- Golang · Go教程 | 37分钟前 | Go time.LoadLocation time.ParseInLocation 时区解析
- time.ParseInLocation 解析无时区时间的时区选择方法
- 255浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 997次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 1000次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 1121次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 1096次使用
-
- MMBench
- MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
- 937次使用
-
- GScript 编写标准库示例详解
- 2022-12-30 369浏览
-
- 关于Golang标准库flag的全面讲解
- 2023-02-25 344浏览
-
- Go代码规范错误处理示例经验总结
- 2022-12-23 278浏览
-
- Golang标准库unsafe源码解读
- 2022-12-29 464浏览
-
- 快速掌握Go语言HTTP标准库的实现方法
- 2022-12-30 327浏览

