Gin的recovery中间件只捕获panic,不处理显式返回的error;需自定义错误中间件统一拦截handler返回的error并响应。

为什么 Gin 的 recovery 中间件拦不住你自定义的错误
因为 recovery 只捕获 panic,不处理 return errors.New(...) 这类显式错误。你写的 c.AbortWithError(500, err) 或 c.JSON(500, gin.H{"error": err.Error()}) 根本不会触发它——它压根没 panic。
常见错误现象:panic: runtime error: invalid memory address 被拦了,但业务校验失败返回的 400 Bad Request 却散落在各处,日志格式不统一、错误码不收敛、前端还要自己解析不同字段。
- 真正要拦截的是「业务逻辑中主动返回的错误」,不是 panic
- 必须在路由注册前,用
Use()加载自定义中间件,且顺序要在recovery之后(否则 panic 会提前终止流程) - 不要在 handler 里直接
c.JSON错误,统一交给中间件响应,否则中间件拿不到错误上下文
怎么让所有 handler 的错误都走同一个出口
核心是约定:所有 handler 函数返回 error,中间件统一检查并渲染。Gin 本身不强制这个模式,得自己封装一层。
实操建议:
立即学习“go语言免费学习笔记(深入)”;
- 定义统一的错误结构体,比如
type AppError struct { Code int `json:"code"` Msg string `json:"msg"` } - handler 改成函数签名
func(c *gin.Context) error,而不是func(c *gin.Context) - 写中间件检查
err := handler(c),非 nil 就调用c.AbortWithStatusJSON(...)渲染 - 注意:
c.Next()不适合这种模式,要用c.Set("handler", yourHandler)+ 手动执行,或改用第三方库如gin-contrib/abort
示例片段:
func ErrorHandler() gin.HandlerFunc {
return func(c *gin.Context) {
c.Next()
if len(c.Errors) > 0 {
// 或者从 c.Get("app_error") 取
c.AbortWithStatusJSON(400, gin.H{"error": c.Errors.ByType(gin.ErrorTypeAny).Last().Err.Error()})
}
}
}
AbortWithError 和 AbortWithStatusJSON 到底该用哪个
AbortWithError 是 Gin 内部错误收集机制,只把错误塞进 c.Errors,不自动响应;AbortWithStatusJSON 是立即中断并写响应体。二者目的不同,别混用。
使用场景:
- 想记录错误但由后续中间件决定怎么响应 → 用
c.AbortWithError(statusCode, err) - 想立刻返回 JSON 并终止链路 → 用
c.AbortWithStatusJSON(statusCode, data) - 如果用了
AbortWithError却没配错误收集中间件,错误就丢了,前端收不到任何响应 -
AbortWithStatusJSON会覆盖已写入的 header,若之前调用过c.Header(),可能被清掉
日志和错误码怎么对齐,避免运维查问题时抓瞎
关键不是记录「发生了什么错误」,而是记录「谁、在什么上下文、触发了哪条业务路径的哪个错误码」。
容易踩的坑:
- 只打
log.Println(err),没带上c.Request.URL.Path、c.GetString("user_id")等上下文 - 错误码硬编码在 handler 里,比如
c.AbortWithStatusJSON(422, ...),后期无法全局审计或翻译 - 用字符串拼接错误信息(
"failed to parse " + field),导致日志无法结构化提取 - 没区分错误等级:
AppError{Code: 500}和AppError{Code: 401}都打 INFO 日志,告警规则没法配置
建议直接用 zap 或 zerolog 记录结构化日志,字段包含:path、method、status、error_code、error_msg、trace_id。
复杂点在于:错误码语义必须和 HTTP 状态码解耦。比如业务拒绝请求是 ERR_INSUFFICIENT_BALANCE,对应 HTTP 402,但未来可能改成 400+ 自定义 code 字段——这个映射关系得集中管理,不能散落各处。










