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

Go errors.Is 为什么不会反向调用目标值的 Is

来源:17golang原创

时间:2026-09-27 22:21:47 301浏览 收藏

errors.Is(err, target) 不会反向调用 target.Is(err),因为它的契约是搜索第一个参数 err 的错误树。对树中的每个当前节点,标准库先判断它是否等于 target,再看这个当前节点是否实现 Is(error) bool 并调用 current.Is(target)。target 只是匹配目标,不是另一个需要遍历的错误树。

官方文档:https://pkg.go.dev/errors#Is

要点速览
  • 匹配方向固定为 err 到 target,不是双向协议。
  • 自定义 Is 应写在会出现在错误树里的具体错误类型上。
  • Is 方法只做浅比较,不应在方法内部调用 Unwrap。

errors.Is 的方向由第一个参数决定

我第一次写自定义错误匹配时,把 Is 放到了“目标错误”类型上,直觉是目标应该决定谁能匹配自己。结果方法完全没有触发。重新看官方定义后才发现,errors.Is 从来不是对两个值做对称协商:它把 err 当作待搜索的树,把 target 当作查询条件。

检查顺序可以概括为:当前错误与目标直接相等;若不相等,则调用当前错误自己的 Is(target);仍不匹配时,再按照 Unwrap() error 或 Unwrap() []error 展开子节点。多子节点按先序深度优先检查。

errors.Is 在 err 错误树中检查当前节点、Is 方法和 Unwrap 子节点的单向匹配边界
图1:errors.Is 单向匹配结构图,左侧是被搜索的 err 树,右侧 target 只作为比较参数;这是静态说明图,不是运行截图。

反向实现 Is 为什么没有效果

下面的目标类型即使把 Is 写成永远返回 true,也不会让普通源错误匹配成功,因为 GreedyTarget.Is 不在调用路径上:

type GreedyTarget struct{}

func (*GreedyTarget) Error() string { return "target" }

func (*GreedyTarget) Is(error) bool {
    // 这个方法位于 target 一侧,errors.Is 不会反向调用它
    return true
}

source := errors.New("source")
target := &GreedyTarget{}

// 结果仍为 false:source 不等于 target,也没有 source.Is(target)
matched := errors.Is(source, target)

这不是遗漏,而是 API 的方向性。若目标值也能主动决定匹配,错误树里的节点和查询目标会同时影响语义,调用方很难判断由谁负责兼容。标准库把责任放在被检查的错误节点上,因此一个错误类型可以声明“我等价于某个公共哨兵错误”。

把自定义 Is 写在产生错误的一侧

更常见的做法是让业务错误根据稳定字段匹配目标。下面的 CodeError 出现在返回链中,所以它的 Is 会被调用:

type CodeError struct {
    Code string
    Err  error
}

func (e *CodeError) Error() string { return e.Code }

func (e *CodeError) Unwrap() error {
    // 包装原因交给 errors.Is 统一遍历
    return e.Err
}

func (e *CodeError) Is(target error) bool {
    // 只比较当前节点和目标,不在这里递归展开错误链
    t, ok := target.(*CodeError)
    return ok && e.Code == t.Code
}

var ErrTimeout = &CodeError{Code: "timeout"}

func loadProfile() error {
    cause := &CodeError{Code: "timeout"}
    return fmt.Errorf("load profile: %w", cause)
}

err := loadProfile()
// 指针不相等,但错误树中的 CodeError.Is 会按 Code 匹配
if errors.Is(err, ErrTimeout) {
    fmt.Println("可以按超时策略处理")
}
CodeError、Is target、Unwrap 原因、哨兵目标和调用方之间的静态契约关系
图2:自定义 Is 契约结构图,重点看错误类型如何同时提供浅匹配和包装关系;这是静态说明图,不是运行证据。

如果只需要匹配一个固定哨兵,也可以直接返回 target == ErrTimeout。若目标带字段,字段必须代表稳定兼容语义;不要把易变文案、时间或堆栈字符串当成匹配条件。

包装树与边界条件

位置errors.Is 的处理
当前节点先做直接相等,再尝试当前节点的 Is(target)
target作为比较参数,不调用它的 Is,并应为可比较值
单个包装通过 Unwrap() error 继续检查
多错误包装通过 Unwrap() []error 深度优先检查子树

自定义 Is 返回 false 不会终止整棵树的搜索,标准库仍会继续展开该节点。也正因为遍历由外层统一完成,官方要求 Is 只做浅比较,不要自行调用两侧的 Unwrap。这样包装、连接错误和自定义等价关系才能组合,而不会重复递归。

交换 errors.Is 的两个参数可以吗?

通常不可以。交换后被搜索的错误树也变了,原错误链不会再被遍历,自定义方法的接收者也会改变。

target 必须是哨兵错误吗?

不必须,但它应是可比较且语义稳定的错误值。常见做法是包级哨兵,或只携带稳定匹配字段的目标对象。

自定义 Is 能同时保留底层原因吗?

可以。类型同时实现浅层 Is 和 Unwrap,前者表达等价关系,后者把底层原因交给标准库继续遍历。

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