直接用 errors.New 无法提取错误码,因其返回的 error 实例仅含字符串、无结构字段或 Code() 方法;必须定义实现 error 接口并带 Code() 方法的结构体(如 AppError),再通过 errors.As 提取。

为什么直接用 errors.New 无法提取错误码
因为 errors.New 返回的是一个只含字符串的不可变 error 实例,没有任何结构字段或方法供你读取错误码。一旦发生错误,你只能靠 err.Error() 解析字符串——这脆弱又易出错,比如日志里多加个空格就匹配失败。
真正能提取错误码的前提,是让错误类型自己“带码出厂”:定义一个结构体实现 error 接口,并暴露 Code() 方法或导出字段。
常见错误现象:if strings.Contains(err.Error(), "invalid_token") —— 别这么干,这是在给未来埋雷。
定义带错误码的自定义错误类型(Go 1.13+ 推荐方式)
Go 1.13 引入了 errors.Is 和 errors.As,所以推荐用结构体 + 方法的方式,兼顾可判断、可提取、可嵌套:
type AppError struct {
Code int
Message string
Err error // 可选:用于链式错误(如底层 io.ErrUnexpectedEOF)
}
func (e *AppError) Error() string {
return e.Message
}
func (e *AppError) Code() int {
return e.Code
}
func (e *AppError) Unwrap() error {
return e.Err
}
使用时直接构造:
return &AppError{Code: 401, Message: "token expired", Err: io.EOF}
提取错误码就很简单:
var appErr *AppError
if errors.As(err, &appErr) {
log.Printf("error code: %d", appErr.Code())
}
- 必须用指针类型
*AppError声明变量,否则errors.As匹配失败 -
Unwrap()方法不是必须的,但加上后支持errors.Is向下穿透(比如判断是否由io.EOF导致) - 如果错误码是字符串(如
"ERR_TIMEOUT"),把Code字段和Code()方法类型改为string即可
如何避免在 HTTP handler 中重复写错误码映射逻辑
很多人在 handler 里写一堆 switch err.(type) 或 if errors.As(...) 来决定返回什么 HTTP 状态码,既冗余又难维护。
更稳妥的做法是让错误类型自己“知道怎么转成 HTTP 状态”:
func (e *AppError) StatusCode() int {
switch e.Code {
case 400, 401, 403:
return e.Code
case 500:
return http.StatusInternalServerError
default:
return http.StatusInternalServerError
}
}
然后在统一的错误中间件里调用:
func errorHandler(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if rec := recover(); rec != nil {
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "internal error"})
}
}()
next.ServeHTTP(w, r)
})
}
// 在 handler 内部:
if err != nil {
var appErr *AppError
if errors.As(err, &appErr) {
w.WriteHeader(appErr.StatusCode())
json.NewEncoder(w).Encode(map[string]interface{}{"code": appErr.Code(), "message": appErr.Error()})
return
}
// 其他未识别错误兜底
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(map[string]string{"error": "unknown error"})
return
}
- 别在每个 handler 里手动写
switch appErr.Code,抽到方法里复用 - 注意:如果
AppError是从第三方库返回的(比如数据库驱动),它可能没实现StatusCode(),此时应先做类型断言再调用 - HTTP 状态码不等于业务错误码,不要强行一一对应;400/401/403 可直透,其他建议统一转为 500 或 400,避免泄露服务内部细节
用 fmt.Errorf 嵌套错误时还能提取原始错误码吗
可以,但前提是原始错误实现了 Unwrap() 并且你用 errors.As 循环展开。Go 的错误链会保留所有嵌套层级。
比如:
err := &AppError{Code: 404, Message: "user not found"}
wrapped := fmt.Errorf("failed to process user: %w", err)
这时仍能提取原始码:
var appErr *AppError
if errors.As(wrapped, &appErr) {
// ✅ 成功,appErr.Code() == 404
}
关键点:
- 必须用
%w(不是%s)格式化动词,否则错误链断裂 -
errors.As会自动遍历整个错误链,找到第一个匹配的*AppError - 如果中间某层用了
%v或fmt.Sprintf拼接,那原始错误对象就丢了,再也无法提取码
最容易被忽略的是:团队协作中,有人随手把 err = fmt.Errorf("xxx: %v", err) 当成习惯,结果整条链上只剩字符串——这时候再想提取错误码,只能重写。


















