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

Go errors.Is 怎样判断包装错误是否属于目标错误

来源:17golang原创

时间:2026-10-20 19:46:04 208浏览 收藏

项目里经常会给底层错误补充业务上下文,例如把“文件不存在”改成“读取配置失败:文件不存在”。如果上层继续用 err == target,包装之后就可能判断失败。真正要问的是:目标错误是否仍在当前错误树里,而不是两段错误文本是否相同。

官方资料:https://pkg.go.dev/errors#Is

用 fmt.Errorf("...: %w", err) 保留可遍历的错误链,再用 errors.Is(err, target) 判断目标错误;直接相等只适合你明确需要同一个错误值的场景。
  • errors.New 每次创建的错误值都不同,文本相同不代表值相等。
  • %w 会保留包装关系,%v 只负责格式化文本。
  • 自定义 Is 只应表达当前错误与目标错误的浅层等价关系。

先看清直接比较失败的原因

下面的代码有两个容易混淆的事实:两次调用 errors.New 得到的是两个不同的错误值;而 fmt.Errorf 返回的又是一个新的包装错误。即使最终打印出来的文本相似,指针和值也未必相同。

package main

import (
	"errors"
	"fmt"
)

var ErrMissing = errors.New("配置项不存在") // 哨兵错误:整个包复用同一个值

func loadConfig() error {
	return fmt.Errorf("读取服务配置失败: %w", ErrMissing) // %w 保留底层错误关系
}

func main() {
	err := loadConfig()
	fresh := errors.New("配置项不存在") // 文本相同,但不是 ErrMissing
	fmt.Println(err == ErrMissing) // 包装错误与底层错误不是同一个值
	fmt.Println(err == fresh)      // 新建错误值也不会因为文本相同而相等
	fmt.Println(errors.Is(err, ErrMissing)) // 沿错误链检查哨兵错误
}

这段代码里,前两个比较表达式都不适合表达“底层原因是否为配置缺失”。errors.Is 会先检查外层错误,再沿 Unwrap 关系寻找目标,因此它能识别被上下文包住的 ErrMissing。

Go errors.New、fmt.Errorf百分号w和errors.Is组成错误树的静态关系说明图
图1:错误包装结构说明图,展示外层上下文、%w 连接和 errors.Is 目标匹配的关系;这是静态说明图,不是运行截图。

用 %w 建立能被检查的错误树

在边界层补充上下文时,%w 是关键。它让返回值同时具备可读的描述和可遍历的底层错误;如果使用 %v,字符串仍然能打印出来,但错误链不会因为这次格式化自动建立。

func readSettings() error {
	if err := openSettings(); err != nil {
		return fmt.Errorf("初始化设置: %w", err) // 保留下层原因,方便调用方判断
	}
	return nil
}

func openSettings() error {
	return ErrMissing // 返回稳定的哨兵错误,调用方可用 errors.Is 识别
}

func handle() error {
	err := readSettings()
	if errors.Is(err, ErrMissing) { // 判断原因,不依赖错误文本
		return fmt.Errorf("请先创建设置文件: %w", err) // 继续补充上下文
	}
	return err
}

错误链可以有多层,调用方不需要知道中间经过了多少层包装。需要注意的是,只有确实希望上层依赖底层错误语义时才使用 %w;一旦暴露了某个哨兵错误,后续替换实现时就要考虑兼容这项可观察行为。

按错误树理解 errors.Is 的匹配范围

官方文档把错误树定义为当前错误以及通过 Unwrap() error 或 Unwrap() []error 得到的子错误。errors.Is 会先看当前节点,再按深度优先方式遍历子节点;因此它处理的不只是单层 fmt.Errorf。

func combine() error {
	first := fmt.Errorf("读取用户配置: %w", ErrMissing) // 第一条分支保留缺失原因
	second := errors.New("网络不可用")               // 第二条分支代表另一种原因
	return errors.Join(first, second)                 // 把多个原因组成一棵错误树
}

func check() {
	err := combine()
	fmt.Println(errors.Is(err, ErrMissing)) // 第一分支命中
	fmt.Println(errors.Is(err, errors.New("网络不可用"))) // 新值文本相同仍不会命中
}

errors.Join 中的每个非空错误都可以被 errors.Is 检查,但目标错误本身必须是可比较的。对于需要提取具体类型或字段的情况,应考虑 errors.As 或较新的 errors.AsType,不要把“是否属于某个类型”和“是否匹配某个错误值”混成一个判断。

Go errors.Is在单错误链和多错误树中按目标值匹配的静态边界图
图2:errors.Is 匹配边界说明图,展示单链、多分支错误树、目标错误和类型提取之间的静态关系;这是结构图,不是运行证据。

哨兵错误与自定义 Is 方法怎样选

固定语义适合用包级哨兵错误,例如 ErrMissing、ErrPermission。调用方只关心“是不是这一类原因”,就用 errors.Is;调用方还需要路径、操作名等字段,就改用 errors.As 提取具体错误类型。

type quotaError struct {
	Limit int
}

func (e quotaError) Error() string {
	return fmt.Sprintf("超过配额 %d", e.Limit)
}

func (e quotaError) Is(target error) bool {
	_, ok := target.(quotaError) // 只表达同一错误类别的浅层匹配
	return ok
}

func checkQuota(err error) bool {
	return errors.Is(err, quotaError{}) // 不比较具体 Limit,避免把字段语义误当成类别
}

自定义 Is 不应递归调用 Unwrap,也不应把所有错误都判成匹配。它负责当前类型和目标之间的浅层等价关系,错误树的遍历仍由 errors.Is 负责。若目标需要携带数据,采用自定义类型加 errors.As 更直接。

常见问题

错误消息一样,为什么 errors.Is 还是 false?

errors.New 每次返回新的错误值。要稳定匹配,请声明并复用同一个哨兵错误,或为错误类型实现清晰的 Is 方法。

%v 能不能代替 %w?

如果上层只需要展示文本可以使用 %v;如果调用方还要识别底层原因,必须保留 %w 形成错误链。

errors.Is 能不能拿来判断错误类型?

它主要匹配目标错误值或目标错误定义的 Is 规则。需要读取具体类型字段时,使用 errors.As 或 errors.AsType。

排查包装错误时,可以固定成三步:先确认目标是错误值还是错误类型,再检查边界是否使用了 %w,最后用 errors.Is 遍历错误树。这样既保留了上下文,也不会把字符串格式当成错误语义。

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