错误处理模式与包装
第 7 章讲了 error 基础与 %w 包装。本章进阶:如何设计可维护的错误体系。
1. 哨兵错误(Sentinel Errors)
预定义包级变量,调用方用 errors.Is 判定:
var ErrNotFound = errors.New("not found")
var ErrPermission = errors.New("permission denied")
func Get(id string) (*User, error) {
if !exists(id) {
return nil, ErrNotFound
}
// ...
}💡用 Is 而非 ==
总是用 errors.Is(err, ErrNotFound),而不是 err == ErrNotFound——因为对方可能用 %w 把哨兵包在更深的链里。
2. 错误类型(Error Types)
需要携带结构化字段时,定义类型:
type ErrConflict struct {
Resource string
ID string
}
func (e *ErrConflict) Error() string {
return fmt.Sprintf("%s %s 冲突", e.Resource, e.ID)
}
// 调用方用 errors.As 取出
var ec *ErrConflict
if errors.As(err, &ec) {
log.Printf("资源 %s 冲突", ec.Resource)
}3. 不透明错误(Opaque Errors)
对外只暴露「成功或失败」,不暴露内部类型——调用方不该依赖你的错误细节:
func Do() error { /* 内部随便怎么包装 */ }
if err != nil {
return fmt.Errorf("do failed: %w", err) // 只透传,不假设类型
}4. 包装链与上下文化
每层加自己的上下文,最底层保留原始错误:
if err != nil {
return fmt.Errorf("保存用户 %s: %w", name, err)
}这样上层的日志能显示完整链路:保存用户 Alice: 写入数据库: 连接超时。
⚠️别滥用 WithStack
%w 已足够构建链。盲目加堆栈会让错误信息冗长、难以阅读。
5. 处理准则速查
- 能判定的用
Is(哨兵) - 要取字段用
As(类型) - 对外暴露用不透明错误
- 每层补充上下文,但只包装一次
小结
- 哨兵错误用
errors.Is判定,别用== - 需结构化信息用错误类型加
errors.As - 对外接口优先不透明错误,隐藏实现
%w构建可追溯的上下文化错误链- 判定用 Is/As,包装用
%w,分层补充上下文