默认 gin.Default() 的 recovery 不够用,因其仅调用 c.AbortWithStatus(500) 返回空白响应、无结构化错误字段、日志缺失 request_id 且抢占中间件顺序导致自定义 recover 失效;必须禁用默认 recovery,改用 gin.New() 并手动注册 CustomRecovery 中间件于 r.Use() 链最前端,确保 panic 可捕获、响应 JSON 化、日志带上下文,并严格检查 c.Writer.Written() 防重复写头。

为什么默认 gin.Default() 的 recovery 不够用
它只调用 c.AbortWithStatus(500),返回空白响应体,不写 JSON、不透出 code 或 message 字段,前端收不到结构化错误;日志里也只打堆栈,不带 request_id 或上下文,排查困难。更关键的是:它抢在你自定义中间件前执行,你写的 defer recover() 根本捕不到 panic。
必须禁用默认 recovery 并手写中间件
用 gin.New() 替代 gin.Default(),显式注册自己的 recovery 中间件,且必须放在 r.Use() 链最前面:
- 先调
r.Use(CustomRecovery()),再加gin.Logger()、鉴权等其他中间件 - 绝对不要在
CustomRecovery()里调c.Next()后还继续执行业务逻辑——recover 后必须立刻终止流程 - 检查
c.Writer.Written(),避免重复写响应头导致http: multiple response.WriteHeader calls
示例中间件片段:
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
if c.Writer.Written() {
return
}
stack := string(debug.Stack())
log.Printf("[PANIC] %s %s: %v\n%s", c.Request.Method, c.FullPath(), err, stack)
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"message": "服务暂时不可用",
"request_id": c.GetString("request_id"),
})
}
}()
c.Next()
}
}
业务错误和 panic 要走两套正交路径
panic 是程序崩溃,靠 recover 拦;业务错误(如参数校验失败、DB 查询为空)不是 panic,必须显式调用 c.Error() 注入,再由另一个中间件统一读取 c.Errors 渲染响应:
- handler 里别写
return errors.New("xxx")—— Gin 完全无视返回值 - 要写
c.Error(&AppError{Code: 4001, Message: "用户名已存在"}),然后return - 后续中间件遍历
c.Errors.ByType(gin.ErrorTypeAny)取第一个非 nil 错误,按类型匹配并转成对应 HTTP 状态码和 JSON
漏掉任意一条,错误就静默丢失或返回 200 + 空数据。
协程里的 panic 必须手动 recover
Gin 的 recovery 中间件只管主 goroutine,你在 handler 里起的 goroutine(比如异步发消息、写日志)一旦 panic,整个进程会退出。这类代码必须自己包 defer recover():
- 别在 goroutine 里直接
panic(err),改用log.Error(...)+ 通知机制 - 如果真要 panic(极少见),必须在 goroutine 内部
defer func(){ if r := recover(); r != nil { ... } }() - 注意:goroutine 中的
c对象不能跨协程安全使用,尤其c.Request.Body已关闭,访问会二次 panic
最稳妥的做法是——根本不在 handler 启动 goroutine 处理核心逻辑,把异步操作下沉到 service 层并封装错误回调。


















