gin.recovery()只打日志不返回json,因其直接调用http.error(w, "internal server error", 500)绕过gin响应流程,不检查c.writer.written(),也不结构化输出。

gin.Recovery() 为什么只打日志不返回 JSON?
因为 gin.Recovery() 默认行为是调用 log.Print 输出堆栈到 os.Stderr,然后直接调用 http.Error(w, "Internal Server Error", 500) —— 它不走 Gin 的响应流程,也不检查 c.Writer.Written(),更不会结构化返回 JSON。你在生产环境看到空白 500 或浏览器提示“无法加载资源”,就是这个原因。
常见错误是直接注册 router.Use(gin.Recovery()) 就以为万事大吉,结果 panic 后前端收不到 {"code":500,"message":"..."},监控系统也抓不到结构化错误字段。
- 它用的是
debug.Stack(),但默认截断严重(仅约 48 字节),看不到业务代码行号 - 不暴露
*http.Request实例,没法记录X-Request-ID、User-Agent或原始body - 堆栈写死到 stderr,无法重定向到文件或 sentry
自定义 Recovery 中间件必须包裹 c.Next() 才生效
panic 只有发生在 c.Next() 执行期间及后续调用链中,才会被 defer 里的 recover() 捕获。如果把可能 panic 的操作(比如解包空指针、强制类型断言)写在 c.Next() 前面,或者漏写 c.Next(),那 panic 就直接穿透出去,服务照样崩。
典型翻车写法:
func BadRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
c.JSON(500, gin.H{"error": "never reached"})
}
}()
// ❌ 这里就 panic 了:c.Param("id") 返回空字符串,再转 int 就 panic
id, _ := strconv.Atoi(c.Param("id"))
c.Next() // panic 已经发生,这行根本没机会执行
}
}
- 正确顺序:defer →
c.Next()→ panic 发生在业务 handler 或下游中间件里 - 确保
Recovery在中间件链中靠后注册,比如在gin.Logger()之后,但要在路由匹配之前 - 别依赖框架自动注入的 context 变量(如
c.MustGet("user")),强制断言前先用c.Get("user")判断是否存在
goroutine 内 panic 无法被主 Recovery 拦截
Go 的 recover() 只对当前 goroutine 有效。你在 handler 里起一个 go func() { panic("boom") }(),主 goroutine 的 defer 完全看不见——它会静默退出,无日志、无上报、无重试。
真实场景包括:异步写审计日志、发 MQ 消息、清理 Redis 缓存时 panic。
- 必须在每个子 goroutine 内部单独加
defer/recover - 子 goroutine 里禁止调用
c.JSON()、c.Abort()等操作响应的方法,否则会 panic - 推荐封装复用:
go safeRun(func() { /* 业务逻辑 */ }),其中safeRun包含 recover + log - 如果真要跨 goroutine 传递错误,用
errgroup.Group或 channel + select 控制生命周期
recover 后写响应前必须检查 c.Writer.Written()
panic 可能发生在已经调用过 c.JSON(200, data) 之后(比如写完 body,又在 defer 清理资源时 panic)。此时再调 c.JSON(500, ...) 会触发 "http: multiple response.WriteHeader calls" 新 panic。
正确做法不是靠运气跳过,而是用状态标记:
if !c.Writer.Written() {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{
"code": 500,
"message": "服务暂时不可用",
})
}
-
c.AbortWithStatusJSON()会自动调用c.Abort(),阻止后续中间件执行 - 不要用
http.Error(),它绕过 Gin 的 writer 封装,无法感知是否已写 header - 开发环境可加
"stack": string(debug.Stack()),但生产环境必须删掉——堆栈可能含敏感路径或变量名 - 堆栈深度建议设 ≥32,用
debug.Stack()而非debug.PrintStack(),后者直接输出到 stderr 不可控
实际部署时最容易被忽略的点:请求 body 只能读一次。如果业务逻辑需要 body 内容,必须在 Recovery 中间件开头就用 io.ReadAll(c.Request.Body) 保存,否则 recover 时 body 已关闭,连原始参数都拿不到。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











