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

Go errors.Is 匹配不到包装错误的 Unwrap 设计

来源:17golang原创

时间:2026-10-28 05:53:37 115浏览 收藏

如果一层自定义错误已经把原始错误包进去了,errors.Is 却仍然返回 false,先不要改匹配目标。最常见的原因是:包装类型只实现了 Error(),没有实现 Unwrap() error;或者使用 %v 把底层错误拼进了文本,却没有保留错误关系。

errors.Is 匹配的不是错误消息,而是当前错误及其 Unwrap 子错误组成的错误树。让自定义包装类型实现正确的 Unwrap,并用 %w 传递包装关系,通常就能恢复匹配。

errors.Is 实际检查的是一棵错误树

标准库文档把包装错误定义为:错误类型提供 Unwrap() error 或 Unwrap() []error,调用方就可以继续看到它包住的错误。errors.Is(err, target) 会先检查 err 自身,再按树结构检查子错误,所以“打印出来一样”不等于“可以匹配”。

errors.Is、外层包装错误、Unwrap、内部原因和匹配结果的错误树静态关系说明图
图1:errors.Is 沿错误树查找目标错误的静态关系说明图,不是运行截图。
package main

import (
    "errors"
    "fmt"
)

var ErrQuota = errors.New("quota exceeded")

type quotaError struct {
    cause error
}

func (e *quotaError) Error() string {
    // Error 负责给日志提供可读文本,不负责暴露错误树关系。
    return "save failed: " + e.cause.Error()
}

func (e *quotaError) Unwrap() error {
    // Unwrap 让 errors.Is 能继续检查真正的原因。
    return e.cause
}

func main() {
    err := "aError{cause: ErrQuota}
    fmt.Println(errors.Is(err, ErrQuota)) // 输出 true
}

这里的关键不是 Error 返回了什么字符串,而是 Unwrap 返回了 ErrQuota。如果删掉这个方法,外层错误仍然能打印出配额信息,但 errors.Is 没有继续下钻的入口。

为什么 %v 包装会让 errors.Is 失去目标

fmt.Errorf 中的 %w 会把错误参数保留为可展开的包装关系;%v 只参与格式化,结果是新的文本错误。两段日志可能完全相同,但只有前者能让 errors.Is 找到原始目标。

%v 文本结果、%w 包装、Unwrap 单错误、多错误和自定义 Is 的静态设计关系图
图2:错误包装 API 与 errors.Is 匹配能力的静态设计边界,不是运行截图。
package main

import (
    "errors"
    "fmt"
)

var ErrDecode = errors.New("decode failed")

func keepRelation() error {
    // %w 保留底层错误,调用方可以继续使用 errors.Is。
    return fmt.Errorf("read config: %w", ErrDecode)
}

func keepTextOnly() error {
    // %v 只保留文本,不把 ErrDecode 放进可遍历的错误树。
    return fmt.Errorf("read config: %v", ErrDecode)
}

func main() {
    fmt.Println(errors.Is(keepRelation(), ErrDecode))  // 输出 true
    fmt.Println(errors.Is(keepTextOnly(), ErrDecode)) // 输出 false
}

排查时可以先看包装点:如果这是单一底层原因,优先使用 %w 或实现 Unwrap() error。不要为了让匹配通过而比较错误字符串;字符串是给人看的诊断信息,错误树才是给程序判断的关系。

多错误和自定义 Is 只在确有需要时扩展

一个错误确实同时代表多个原因时,可以让类型实现 Unwrap() []error,或者使用标准库提供的多错误组合方式。这样错误树会出现多个子节点,errors.Is 会逐个检查它们。若业务需要“语义等价”而不是指针或值的直接匹配,再为包装类型提供 Is(error) bool,并把规则限定在清晰的目标类型上。

type batchError struct {
    causes []error
}

func (e *batchError) Error() string {
    // 这里只汇总人类可读信息,匹配仍交给错误树。
    return "batch failed"
}

func (e *batchError) Unwrap() []error {
    // 返回的每个非 nil 子错误都可以成为 errors.Is 的检查分支。
    return e.causes
}

多错误不是单错误包装的替代写法:只有在“多个原因同时成立”时才使用它。自定义 Is 也不应该把所有错误都判成相等,否则调用方会失去对具体原因的判断能力。

用最小模板复查 Unwrap 设计

现象优先检查处理方向
日志里有底层错误,errors.Is 返回 false包装类型是否有 Unwrap单一原因实现 Unwrap() error
fmt.Errorf 后无法匹配动词是否写成 %v需要传递关系时改用 %w
一个错误包含多个原因是否真的存在并列原因使用 Unwrap() []error 或多错误组合
目标不是同一个错误值是否需要语义匹配谨慎实现自定义 Is,并限制目标范围

最后可以按三个问题收口:这是单一原因还是多个原因?包装层有没有把原因暴露给错误树?调用方要匹配具体错误值,还是只匹配一种明确语义?答案一致时,errors.Is 的行为通常就不会再靠猜。

相关问题

  • 自定义错误只实现 Error 方法时,为什么日志正常但 errors.Is 失败?
  • 什么时候应该用 errors.As,而不是 errors.Is?
  • Unwrap() []error 返回 nil 子错误会带来什么问题?
  • 如何测试包装错误既保留上下文又能匹配目标?
声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>