必须自写CustomRecovery中间件并置于r.Use()最前,因gin.Recovery()仅打印堆栈并返回固定{"error":"Internal Server Error"},不触发统一响应逻辑;需defer+recover捕获panic,用debug.Stack()记录日志,c.AbortWithStatusJSON()返回结构化500响应。

必须自己写 recovery 中间件,禁用 gin.Default() 自带的 Recovery,否则 panic 会直接返回空白 500,不走你的任何错误逻辑。
为什么 gin.Recovery() 默认行为不能用
gin.Recovery() 只做两件事:打印 panic 堆栈到日志、调用 c.Abort() 并返回固定字符串 {"error": "Internal Server Error"}。它不调用 c.AbortWithStatusJSON(),也不触发你定义的统一响应结构,更不会记录请求路径、参数或生成 trace ID。你在中间件里写的 c.JSON(500, ...) 完全没机会执行。
常见错误现象:
- 前端收到
{"error": "Internal Server Error"},但后端日志里只有堆栈,没有请求 URL 或 query 参数 - 你写了自定义 recover 中间件,但 panic 从未进入其中——因为
gin.Recovery()已在链中更早位置捕获并吞掉了 - 线上出问题时,只能靠 grep stdout 找堆栈,无法关联监控指标或请求 ID
怎么写一个真正可用的 CustomRecovery 中间件
核心是:用 defer+recover() 捕获 panic,用 debug.Stack() 获取完整堆栈(用于日志),用 c.AbortWithStatusJSON() 立即终止流程并返回结构化响应。
实操要点:
- 中间件必须注册在
r.Use()的最前面,比如r.Use(CustomRecovery(), gin.Logger(), ...) - 务必检查
c.Writer.Written(),避免多次写 response body 导致http: multiple response.WriteHeader calls - 生产环境禁止把
debug.Stack()返回给前端,但日志里必须保留——建议用结构化日志库(如 zap)写入stack字段 - 不要用
fmt.Sprintf("%v", err)拼接错误信息,panic 可能是 string、error 或其他类型,统一兜底为"internal server error"
示例片段:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
stack := string(debug.Stack())
log.Error("panic recovered", zap.String("path", c.Request.URL.Path), zap.String("method", c.Request.Method), zap.String("stack", stack))
if c.Writer.Written() {
return
}
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"message": "service unavailable",
"data": nil,
})
}
}()
c.Next()
}
}
业务错误和 panic 必须分开处理
panic 是程序崩溃级异常(如 nil pointer dereference),而业务错误(如参数校验失败、DB not found)应该用 c.Error() 主动注入,再由另一个中间件统一响应。这两条路径互不干扰,但都依赖你手动控制流程。
关键区别:
-
c.Error(err)不会自动中断 handler,必须配合c.Abort()或提前return,否则后续代码仍会执行 -
c.Errors是个 slice,你得在中间件末尾遍历它,取第一个非 nil 错误,再按类型(比如*app.ValidationError)匹配响应码和 message - 别在 handler 里写
if err != nil { c.JSON(400, ...) }——这会让错误响应结构散落各处,破坏统一性 - 所有业务错误建议封装成自定义 error 类型,比如
ErrInvalidParam = &AppError{Code: 4001, Message: "invalid param"},便于errors.Is()判断
真正容易被忽略的是:c.Error() 注入的错误,不会自动触发任何中间件;你必须显式写一个“错误响应中间件”,且它得放在所有业务中间件之后、CustomRecovery 之后(但 CustomRecovery 必须在最前)。顺序错了,错误就收不到、响应就发不出、日志就对不上。


















