Day 9|错误包装与判断(errors.Is / errors.As)[Go 语言 30 天系统学习计划]

学习目标

  1. 理解 Go 中“错误链(error chain)”是如何工作的
  2. 正确使用 errors.Iserrors.As 判断错误
  3. 掌握哨兵错误与自定义错误在判断时的取舍
  4. 写出不会被重构破坏、不会误判的错误处理代码

一、为什么需要 errors.Is / errors.As

在 Day 8 中你已经学会错误包装:

return fmt.Errorf("query failed: %w", err)

此时,一个 error 的结构可能是:

query failed
 └── db error
     └── io error

问题是:

在最外层代码中,我如何判断“这是不是我真正关心的那个错误?”

如果你还写:

if err == ErrUserNotFound { 
    // ...
}

结论:判断会失败。因为 err 已经被包装过,不再等于最初的那个错误对象。


二、什么是错误链(Error Chain)

示例:

err1 := errors.New("origin error")
err2 := fmt.Errorf("wrap1: %w", err1)
err3 := fmt.Errorf("wrap2: %w", err2)

逻辑结构是:

err3 → err2 → err1

Go 在运行期会保存这条链。

关键点:%w 的唯一作用就是把原始错误挂进错误链中。

如果不用 %w

fmt.Errorf("wrap: %v", err)

那么错误链会在这一层被“截断”。


三、errors.Is:判断“是不是某一个错误”

errors.Is 用来回答:

“这个 error,或它包装链里的任何一个,是不是目标 error?”

基本用法:

if errors.Is(err, ErrUserNotFound) {
    // 命中
}

判断逻辑:

  • 先比较 err 本身
  • 再比较它包裹的 error
  • 一直向下,直到链尾

四、errors.Is 示例(哨兵错误)

定义一个哨兵错误:

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

底层函数返回:

func findUser(id int) error {
    return fmt.Errorf("service error: %w", ErrUserNotFound)
}

调用方判断:

err := findUser(1)

if errors.Is(err, ErrUserNotFound) {
    fmt.Println("user not found")
}

输出结果:

user not found

工程意义:调用方不需要知道错误是在哪一层产生的。即使中间层重构、增加包装,判断逻辑依然稳定。


五、errors.As:判断“是不是某一类错误”

errors.As 用来回答:

“这个 error,或它包装链里的某一个,是不是某个具体类型?”

当你使用的是自定义 error 类型时,必须用 errors.As


六、errors.As 示例(自定义 error)

定义一个自定义 error 类型:

type PermissionError struct {
    UserID int
}

func (e *PermissionError) Error() string {
    return fmt.Sprintf("user %d has no permission", e.UserID)
}

在底层返回:

func check(uid int) error {
    return fmt.Errorf(
        "service failed: %w",
        &PermissionError{UserID: uid},
    )
}

在调用方判断并提取信息:

err := check(2)

var perr *PermissionError
if errors.As(err, &perr) {
    fmt.Println("permission denied, user:", perr.UserID)
}

说明:

  • errors.As 会沿错误链逐层查找
  • 一旦找到匹配类型,就赋值给 perr
  • 必须传入指针(这里是 &perr),以便写入结果

七、errors.Is 与 errors.As 的核心区别

  • errors.Is:判断“是不是某个特定错误值”(适用于哨兵错误),内部基于 == 比较,并沿错误链查找
  • errors.As:判断“是不是某种错误类型”(适用于自定义 error),内部基于类型匹配,并沿错误链查找

一句话总结:值判断,用 Is;类型判断,用 As。


八、错误判断的反模式(非常重要)

反模式 1:通过字符串判断错误(不推荐)

if strings.Contains(err.Error(), "not found") {
    // ❌
}

问题:

  • 极其脆弱
  • 错误文案一改,逻辑就坏
  • 跨语言、跨团队难维护

反模式 2:中间层吞掉错误链

return errors.New("service failed")

问题:

  • 原始错误丢失
  • errors.Is / errors.As 全部失效
  • 排查成本暴涨

九、工程中推荐的错误设计模式

推荐分层策略:

  • 底层(IO / DB):返回原始错误
  • 中层(Service):使用 fmt.Errorf("xxx: %w", err) 包装并增加语义
  • 上层(API / CLI):使用 errors.Is 判断类别,使用 errors.As 提取上下文

预告:Day 10|panic / recover 与程序边界控制

下一天你将彻底搞清楚:

  • panic 的真正定位
  • recover 应该放在哪里
  • 为什么 panic 绝不能当业务逻辑
  • 框架层兜底的正确姿势

发表评论