登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  Golang >  Go问答

Go errors.Join 返回的错误怎么逐个用 errors.Is 判断

来源:17golang原创

时间:2026-09-09 06:06:42 495浏览 收藏

多个操作同时失败时,errors.Join 不会把错误变成只能按换行拆分的文本,而是返回一个包含多个子错误的错误树。判断其中是否有某个已知原因,直接对聚合结果调用 errors.Is 即可;只有需要把每个原因分别展示、统计或记录时,才读取 Unwrap() []error

最小写法是 errors.Is(joined, target)。它会检查聚合错误本身及其子错误,包括被 %w 包装的原因;不要解析 Error() 字符串,也不要用 errors.Unwrap 期待取出 Join 的切片。

errors.Join 为什么能被 errors.Is 命中

errors.Join 从 Go 1.20 开始提供多错误聚合能力。它会忽略传入的 nil,所有参数都是 nil 时返回 nil;非空结果实现的是 Unwrap() []error。标准库把这种结构视为错误树,errors.Is 会从当前节点开始检查,再按深度优先访问子节点。

package main

import (
    "context"
    "errors"
    "fmt"
)

func main() {
    errTimeout := context.DeadlineExceeded
    errCache := errors.New("缓存不可用")
    // 用 %w 保留缓存错误的可匹配关系,同时补充场景信息。
    joined := errors.Join(errTimeout, fmt.Errorf("读取用户资料:%w", errCache))

    // 业务分支按错误身份判断,不依赖 Error() 的显示文本。
    fmt.Println(errors.Is(joined, context.DeadlineExceeded)) // true
    fmt.Println(errors.Is(joined, errCache))                 // true
}
errors.Join 聚合错误、Unwrap 切片和 errors.Is 目标匹配的静态关系图
图1:聚合错误通过 Unwrap() []error 连接到多个原因,errors.Is 可以在这棵错误树中匹配目标。

这里两个判断都可能为 true,因为目标分别位于 Join 的不同分支。目标错误必须是可比较的;如果项目定义了自定义错误类型,也可以实现 Is(error) bool 来声明等价关系。判断成功只说明树中存在匹配项,并不表示所有操作都成功。

怎么对每个已知原因分别判断

“逐个判断”通常不是手写循环,而是对每个业务关心的目标各调用一次 errors.Is。例如批量刷新资料时,可以把超时、权限和缓存故障分别映射到不同处理策略:

func classify(err error) string {
    // 先判断更需要单独处理的业务原因。
    switch {
    case errors.Is(err, context.DeadlineExceeded):
        return "稍后重试"
    case errors.Is(err, errPermission):
        return "提示重新授权"
    case errors.Is(err, errCache):
        return "降级读取"
    default:
        return "记录未知错误"
    }
}

多个目标同时存在时,switch 只会返回第一个命中的分支;如果要同时收集标签,就分别执行判断:

func labels(err error) []string {
    labels := make([]string, 0, 3)
    // 每个目标独立判断,Join 中命中几个就保留几个标签。
    if errors.Is(err, context.DeadlineExceeded) {
        labels = append(labels, "timeout")
    }
    if errors.Is(err, errPermission) {
        labels = append(labels, "permission")
    }
    if errors.Is(err, errCache) {
        labels = append(labels, "cache")
    }
    return labels
}

不要用 err.Error() == "缓存不可用"strings.Contains 替代身份判断。错误文本可以随着上下文变化,errors.Is 才是包装和聚合关系的稳定入口。

需要逐个查看原因时怎么展开错误树

日志、指标或批量响应有时确实需要列出直接子错误。这时使用 Unwrap() []error 的接口断言。注意,errors.Unwrap(err) 只识别返回单个 errorUnwrap() error,对 errors.Join 会返回 nil

type multiUnwrapper interface {
    Unwrap() []error
}

func walk(err error, visit func(error)) {
    if err == nil {
        return
    }
    visit(err)

    // 先展开多错误节点;没有切片时再展开普通 %w 包装。
    if many, ok := err.(multiUnwrapper); ok {
        for _, child := range many.Unwrap() {
            walk(child, visit)
        }
        return
    }
    if one := errors.Unwrap(err); one != nil {
        walk(one, visit)
    }
}
外层错误树、Join 节点、多个子错误和普通 %w 包装的静态关系图
图2:需要展示每个原因时,区分 Join 的 []error 子节点与普通 %w 单链,避免错误地调用 errors.Unwrap。

遍历函数适合做日志明细或错误统计,不适合替代业务分支。遍历时要保留去重策略,否则同一个 sentinel 被多个包装层引用时,展示列表可能重复;而业务是否命中仍然交给 errors.Is

生产环境最容易踩到的四个边界

  • 全是 nil:errors.Join(nil, nil) 返回 nil,调用方可以直接按无错误处理。
  • 嵌套聚合:Join 的子节点还可以是另一个 Join,errors.Is 会继续检查嵌套树。
  • 展示和判断分离:Error() 适合给人看,errors.Is 适合程序判断;不要互相替代。
  • 顺序影响策略:一次只返回一个处理结果时,按业务优先级排列判断;要完整反映状态就独立收集多个命中结果。

速查结论:判断某个原因用 errors.Is(joined, target);判断自定义错误类别用 errors.As;读取 Join 的直接子节点用 Unwrap() []error;不要通过字符串切分恢复错误关系。

相关问题

errors.Join 和 fmt.Errorf 多个 %w 有什么区别?

两者都能形成包含多个子错误的错误树,errors.Iserrors.As 都可以继续匹配;选择时看你是要聚合已有错误,还是要在一段上下文文本中同时包装多个参数。

为什么 errors.Unwrap(errors.Join(...)) 是 nil?

因为 errors.Unwrap 只调用 Unwrap() error,而 Join 实现的是 Unwrap() []error。需要读取子错误时应做接口断言,或直接使用 errors.Is 进行判断。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>