★ 错误链 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 会断链,别拿它包错误。
笔记加载中…