当前位置:首页 > 文章列表 > Golang > Go教程 > errors.Join 聚合多个清理错误的返回方案

errors.Join 聚合多个清理错误的返回方案

来源:17golang原创 2026-11-12 16:37:31 0浏览 收藏

在一个函数里关闭文件、响应体、事务或临时资源时,清理动作往往不止一个。若每次都覆盖前一个错误,调用方最后只能看到最后一次关闭失败,前面的原因就丢了。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 按后进先出执行,最后一次赋值会覆盖前面的错误。这个问题在“主流程成功、清理阶段失败”时尤其容易被忽略:函数表面上返回了错误,但调用方无法知道还有哪些资源没有正确关闭。

Go 清理多个资源并用 errors.Join 聚合错误的关系说明图
图1:多个资源清理错误进入 errors.Join 的静态说明图,不是运行截图。

聚合的重点不是把错误文本拼成一段日志,而是让返回值保留错误树。这样既能让日志看到多个原因,又能让上层继续按具体错误类型做判断。

先收集清理阶段的非 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 保留多个错误的对比说明图
图2:覆盖式返回与 errors.Join 聚合式返回的边界对比图,不是运行截图。
处理方式调用方能看到什么适用情况
每次覆盖返回值通常只剩最后一次清理失败前序清理错误确实不需要保留时
手工拼接字符串文本完整,但失去类型检查仅用于展示日志,不作为程序错误返回值
errors.Join多个错误文本与错误树都保留多个清理动作都影响诊断或补偿时

errors.Join 也不是所有场景都该使用。如果错误之间存在明确的因果链,例如“读取配置失败,因为文件不存在”,应该使用 %w 包装来表达一条链;只有多个相对独立、需要同时呈现的失败,才适合聚合。另一个边界是错误数量:批量处理数万条记录时,不应无上限地把每一条原始错误都放入内存,应该在业务层先做数量限制或汇总。

落地时记住这三个判断

  1. 清理动作相互独立且可能同时失败时,先收集非 nil 错误,再调用 errors.Join。
  2. 主流程已经有错误时,不要用清理错误覆盖它,用 errors.Join(mainErr, cleanupErr) 保留两类原因。
  3. 需要判断具体原因时使用 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

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
slog.Group 将结构化日志按业务域分组的方法slog.Group 将结构化日志按业务域分组的方法
上一篇
slog.Group 将结构化日志按业务域分组的方法
下一篇
下一篇
暂无
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    997次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    1000次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    1121次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    1096次使用
  • MMBench详解:多模态大模型基准测试、功能特点与使用指南
    MMBench
    MMBench是由上海人工智能实验室等机构联合推出的多模态基准测试平台,提供细粒度能力评估、大规模数据集及VLMEvalKit工具。本文详细介绍其核心功能、安装使用方法及应用场景,助力开发者全面评估多模态模型性能。
    937次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议 和 隐私政策
返回登录
  • 重置密码