根本原因是被包装的原始错误未实现Unwrap()方法或用值接收者实现Error();必须用指针接收者实现Error()、显式实现Unwrap()返回嵌套err,且errors.Is匹配需同类型指针。

为什么 fmt.Errorf("%w") 包装后 errors.Is 失效
不是包装语法错了,而是被包装的原始错误没实现 Unwrap() 方法,或者用了值接收者实现 Error()。Go 的 %w 只对实现了 Unwrap() error 的类型生效;如果自定义错误是 func (e MyError) Error()(值接收者),每次调用都生成新实例,errors.Is(err, target) 会因地址不同而失败。
必须满足三点:
-
Error()必须用指针接收者:func (e *MyError) Error() string - 显式实现
Unwrap() error并返回嵌套字段(如e.err) -
errors.Is(err, target)中的target必须是同一类型指针,比如*NotFoundError,不能是值类型或接口变量
HTTP handler 里怎么让错误自带状态码并被统一拦截
别把状态码和错误拆开传——常见错误是返回 errors.New("not found") 后在 handler 里手动判断再写 http.Error(w, ..., 404),容易漏判、错位,甚至静默返回 200。
正确做法是定义接口并让所有业务错误实现它:
立即学习“go语言免费学习笔记(深入)”;
- 定义
HTTPStatusError接口:type HTTPStatusError interface { error; StatusCode() int } - 业务错误结构体字段必须导出(首字母大写),例如
Code int,不能是code int - handler 最外层做类型断言:
if e, ok := err.(HTTPStatusError); ok { http.Error(w, e.Error(), e.StatusCode()) } - 禁止在 service 或 goroutine 内直接调用
http.Error—— 它会绕过中间件,且异步 goroutine 的错误根本进不了 handler 调用栈
哪些错误该包装,哪些该替换
封装不是堆叠层数,而是传递恰当语义。底层技术错误(如 os.IsNotExist、net.ErrClosed)适合包装:保留原始上下文供排查;已知业务语义错误必须替换,不暴露技术细节。
- repository 层遇到
sql.ErrNoRows,应返回&NotFoundError{Resource: "user"},而不是fmt.Errorf("db query failed: %w", err) - 网络超时、连接中断等可重试错误,建议包装并加
Retryable bool字段,方便上层决策 - 校验失败类错误(如参数非法)用
&ValidationError{Field: "email", Msg: "invalid format"}替换,而非包装底层fmt.Errorf - 所有包装必须用
%w,禁用%v或字符串拼接,否则errors.Unwrap()断链
自定义错误类型怎么避免 panic 和 nil 指针
很多人定义 type AppError struct { Code int; Err error },但在 Unwrap() 里直接写 return e.Err,忘了判空——一旦 e.Err 是 nil,上层调用 errors.Is(err, xxx) 就 panic。
- 状态码字段设默认值:
Code int = http.StatusInternalServerError -
Unwrap()开头加守卫:if e.Err == nil { return nil } - 字段名必须大写才能被
errors.As赋值,例如Message string✅,message string❌ - 测试时故意构造
&AppError{Code: 0}或&AppError{Err: nil},验证是否 panic 或返回非法状态码(HTTP 中 0 不合法)
%w。


















