★ 错误链 errors.Is/As 与 fmt.Errorf %w 怎么用?
一句话结论:Go 1.13 起可用 %w 把错误“包一层”同时保留根因,形成错误链;判断“链上是否有某个哨兵错误”用 errors.Is,提取“链上某一具体错误类型以拿到附加信息”用 errors.As;只有能确认错误来自本层且绝无包装时才能用 ==。
三者分工
| API | 语义 | 典型场景 |
|---|---|---|
| fmt.Errorf("...: %w", err) | 包装:附加上下文,保留 Unwrap 链 | 每层追加“在哪个环节失败” |
| errors.Is(err, target) | 沿链比较(== 或 Is 方法) | 判断是否 ErrNotFound 等哨兵错误 |
| errors.As(err, &target) | 沿链匹配类型(或 As 方法) | 取出 *TimeoutError 等拿到字段做决策 |
| errors.Join(...) | 1.20+ 合并多个错误 | 并行任务收集多个失败再统一处理 |
%v 与 %s 只是把错误文本拼进新错误,链条会断;自定义错误实现 Unwrap() error 即可参与链条。
用法示例
var ErrNotFound = errors.New("not found")
func loadUser(id int) error {
if id <= 0 {
return ErrNotFound
}
return fmt.Errorf("load user %d: %w", id, ErrNotFound) // 包装
}
type TimeoutError struct{ Op string }
func (e *TimeoutError) Error() string { return e.Op + ": timeout" }
// 判断:Is 管哨兵,As 管类型
if errors.Is(err, ErrNotFound) { /* 按 404 处理 */ }
var te *TimeoutError
if errors.As(err, &te) { /* 拿到 te.Op 做重试决策 */ }
Is/As 都会沿 Unwrap 一路走到底;As 的目标必须是指向“错误类型或接口”的指针(上面 te 是 *TimeoutError,传入 &te)。
常见追问 / 记忆点
- 追问:什么时候可以 err == ErrNotFound?——能确认没有包装路径时;有中间层就一律用 errors.Is。
- 追问:每层都 %w,错误文本又长又重复怎么办?——每层只加最小上下文,完整日志只在最顶层打一次。
- 记忆点:包装用 %w、判等用 Is、取类型用 As;%v 会断链,别拿它包错误。