学习目标
- 理解 Go 中“错误链(error chain)”是如何工作的
- 正确使用
errors.Is与errors.As判断错误 - 掌握哨兵错误与自定义错误在判断时的取舍
- 写出不会被重构破坏、不会误判的错误处理代码
一、为什么需要 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 → err1Go 在运行期会保存这条链。
关键点:%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 绝不能当业务逻辑
- 框架层兜底的正确姿势