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

Go errors.Is 怎样匹配包装后的哨兵错误

来源:17golang原创

时间:2026-10-23 00:01:51 191浏览 收藏

我在给服务层错误补充“查询用户失败”这类上下文后,最先踩到的坑是:原来可以直接比较的哨兵错误,经过包装就不再等于原变量了。解决办法不是解析 err.Error(),而是用 fmt.Errorf 的 %w 保留原因,再让 errors.Is 判断包装链中是否仍有目标错误。

只要包装时使用 %w,errors.Is(err, target) 就可以沿着错误的 Unwrap 关系寻找目标;直接比较只适合你确实需要判断两个错误值完全相同的场景。

先用 %w 保留底层哨兵错误

哨兵错误通常是包级变量,调用方只关心“是不是这一类错误”。例如用户不存在时,底层可以返回 ErrUserNotFound,服务层增加上下文,但不要把它改成普通字符串:

package user

import (
    "errors"
    "fmt"
)

// ErrUserNotFound 是调用方可以稳定识别的错误类别。
var ErrUserNotFound = errors.New("user not found")

func loadUser(id string) error {
    if id == "" {
        // 这里用 %w 保留底层哨兵,外层才能继续匹配它。
        return fmt.Errorf("load user %q: %w", id, ErrUserNotFound)
    }
    return nil
}

这里的关键不是错误文本,而是 %w 建立了可展开的包装关系。若写成 %v,字符串看起来仍然包含“user not found”,但底层错误不会按可匹配原因保留下来。

服务层通过 fmt.Errorf 百分号 w 保留 ErrUserNotFound 并交给 errors.Is 匹配的错误包装链结构说明图
图1:包装链结构说明图,查看上下文错误如何保留底层哨兵;这是静态说明图,不是运行截图。

errors.Is 会沿包装链匹配目标

调用方不需要知道服务层加了多少层上下文,只需把返回错误和目标哨兵交给 errors.Is:

package main

import (
    "errors"
    "fmt"
)

var ErrUserNotFound = errors.New("user not found")

func findUser(id string) error {
    // 示例只模拟底层返回,不依赖网络或数据库。
    return fmt.Errorf("repository lookup %q: %w", id, ErrUserNotFound)
}

func main() {
    err := fmt.Errorf("request failed: %w", findUser("u-42"))
    // Is 关注包装链中的目标,不依赖每层的文字描述。
    if errors.Is(err, ErrUserNotFound) {
        fmt.Println("按不存在用户处理")
        return
    }
    fmt.Println("按未知错误处理")
}

errors.Is 会先看当前错误,再按 Unwrap() error 展开包装关系。直接比较 err == ErrUserNotFound 在这个例子中会失败,因为最外层已经是新的包装错误值。

实际工作流里,我会把匹配放在错误处理分支的入口:先识别稳定类别,再决定返回码、用户提示或是否记录更详细的上下文。日志仍可保留原始错误,但不要让日志文本承担程序判断职责。

自定义 Is 和错误树会改变匹配范围

标准库还支持两种扩展关系:错误可以实现 Unwrap() error 形成单链,也可以实现 Unwrap() []error 表示多个子错误;错误类型还可以提供 Is(error) bool 做浅层语义匹配。

package main

import "errors"

var ErrPermission = errors.New("permission denied")

type accessError struct {
    operation string
    cause     error
}

func (e *accessError) Error() string { return e.operation + ": " + e.cause.Error() }

// Unwrap 保留底层原因,让 errors.Is 可以继续向下查找。
func (e *accessError) Unwrap() error { return e.cause }

type categoryError struct{ code int }

func (e categoryError) Error() string { return "category error" }

// Is 只做当前层的类别比较,不在这里递归调用 Unwrap。
func (e categoryError) Is(target error) bool {
    other, ok := target.(categoryError)
    return ok && e.code == other.code
}

func main() {
    wrapped := &accessError{operation: "open", cause: ErrPermission}
    _ = errors.Is(wrapped, ErrPermission)
    _ = errors.Is(categoryError{code: 7}, categoryError{code: 7})
}

自定义 Is 适合表达“同一错误类别”的匹配,但它应该是浅层判断,不要在里面再次遍历包装链。对于多个独立原因,可以用 errors.Join 形成错误树;errors.Is 会按树遍历去找目标,而不是只看单一父错误。

错误树、Unwrap 单链、多分支、自定义 Is 方法与 errors.Is 目标哨兵的匹配边界关系说明图
图2:匹配边界关系图,查看错误树与自定义 Is 如何进入 errors.Is 的判断范围;这是静态说明图,不是运行截图。

Is、As 和直接比较分别解决什么问题

这三个写法不要混成一套规则:

需求选择判断对象
是否属于某个稳定错误类别errors.Is哨兵错误或自定义 Is 语义
是否为某种具体错误类型,并取出字段errors.As包装链或错误树中的类型
是否就是同一个错误值==当前值是否与目标完全相同

因此,业务分支通常优先使用 errors.Is;需要读取自定义错误的状态码、路径或重试信息时使用 errors.As;只有 API 明确保证返回同一个值时,直接比较才是清晰的选择。

常见问题

为什么用了 %v,errors.Is 还是匹配不到?

%v 只格式化错误文本,不建立可展开的包装关系。需要保留原因时改用 %w,并确认目标错误本身就是你要匹配的哨兵。

errors.Is 能不能替代错误字符串判断?

能替代绝大多数程序分支中的字符串判断。错误文本适合日志和展示,不适合当稳定协议;如果没有哨兵或类型,先补上明确的错误契约。

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