Gin 的 c.Errors 不能直接当业务错误用,因其仅用于框架层错误、无业务语义、不带 HTTP 状态码且难以区分错误类型,需用 AppError 统一承载状态码与业务码,并通过独立中间件处理。

为什么 Gin 的 c.Errors 不能直接当业务错误用
Gin 内置的 c.Errors 主要用于框架层错误(比如 Bind 失败、Render 异常),它用 ErrorType 位标志区分错误来源,但不承载业务语义。你调用 c.Error(err) 放进去的错误,会被归为 ErrorTypeAny 或 ErrorTypePrivate,前端根本没法区分“参数错”和“数据库连不上”。更麻烦的是,c.Errors 不带 HTTP 状态码字段,c.Next() 后你得自己判断类型再映射,极易漏 case。
gin.RecoveryWithWriter 只管 panic,不管业务错误
很多人以为加了 gin.Recovery() 就万事大吉,其实它只捕获 panic,对 return errors.New("xxx") 这类显式错误完全无感。生产环境必须额外补一层中间件来处理业务错误流。常见错误写法是把 Recovery 和业务错误处理混在一个中间件里,结果 panic 恢复了,但 400 错误却返回了 200 + 错误码,网关和监控全乱套。
- 必须分离 panic 恢复和业务错误处理:前者用
RecoveryWithWriter,后者用独立中间件 -
RecoveryWithWriter的回调函数(如ServerError)只应处理未预期崩溃,不处理ErrNotFound这类可控错误 - 日志写入器(如
lumberjack.Logger)要单独配置,别和业务错误响应逻辑耦合
用高阶函数包装 handler,避免每个路由写 if err != nil
业务函数(比如 userService.CreateUser)通常返回 error,而 Gin handler 签名是 func(*gin.Context)。硬写会导致大量重复:
func CreateUserHandler(c *gin.Context) {
var req CreateUserReq
if err := c.ShouldBindJSON(&req); err != nil {
response.Fail(c, http.StatusBadRequest, 10001, "参数错误")
return
}
if err := userService.CreateUser(&req); err != nil {
response.Fail(c, http.StatusInternalServerError, 50001, "创建失败")
return
}
response.Success(c, nil)
}
更干净的做法是定义一个适配器:
func HandleError(handler func(*gin.Context) error) gin.HandlerFunc {
return func(c *gin.Context) {
if err := handler(c); err != nil {
// 这里统一解析 AppError 类型
if appErr, ok := err.(*AppError); ok {
response.Fail(c, appErr.HttpStatus, appErr.Code, appErr.Msg)
} else {
response.Fail(c, http.StatusInternalServerError, 50000, "系统异常")
}
}
}
}
- 路由注册时直接套一层:
r.POST("/user", HandleError(CreateUserLogic)) -
CreateUserLogic只专注业务,返回*AppError或nil - 所有错误路径收口到
HandleError,不用再担心忘记return或状态码错位
HTTP 状态码和业务码必须严格分离
这是最容易被忽略的设计点。前端靠 HTTP 状态码做重试/跳转(比如 401 触发登录页),靠业务码(code 字段)做 UI 提示(20001 = “余额不足”)。如果把业务错误全塞进 200,网关无法识别真实失败;如果把 404 当业务码返回,前端可能误判为网络问题。
- HTTP 状态码决定请求生命周期:成功(2xx)、客户端错(4xx)、服务端错(5xx)
- 业务码只出现在响应体
code字段,范围建议按模块划分(如 1xxx 参数类,2xxx 业务类,5xxx 系统类) - 不要在中间件里用
switch err.(type)硬编码映射——用AppError结构体显式携带两个码
真正难的不是写中间件,而是让所有人(包括新来的同事)在写 service 层时,本能地返回 &AppError{HttpStatus: 400, Code: 10001, Msg: "xxx"},而不是 errors.New("xxx")。这个契约一旦松动,统一处理就形同虚设。











