默认 gin.Recovery() 返回空白500页面,因其仅打印panic堆栈并调用http.Error,不走JSON响应逻辑、不设Content-Type、不输出code/msg字段,且终止请求导致后续中间件失效,生产环境必须禁用。

为什么默认 gin.Recovery() 返回空白 500 页面
因为 gin.Recovery() 只做两件事:打印 panic 堆栈、调用 http.Error(c.Writer, err.Error(), 500)。它不走你定义的 JSON 响应逻辑,也不调用 c.AbortWithStatusJSON(),所以前端拿到的是原始 HTML 错误页或空响应体。
更关键的是:它不会修改响应头中的 Content-Type,导致浏览器/客户端无法正确解析错误结构——尤其当你统一用 application/json 时,这会直接破坏前后端约定。
- 默认 recovery 不写任何 JSON 字段(如
code、msg),和你的业务错误格式完全割裂 - 它在 panic 后直接终止请求,后续中间件(比如日志、监控)根本收不到上下文
- 生产环境必须禁用它,否则堆栈信息可能泄露到响应体中
怎么用 gin.RecoveryWithWriter() 替换默认行为
这是 Gin 官方提供的可控入口,比手写 defer+recover 更安全——它把 writer 和错误处理函数交给你控制,且保证在标准流程内执行。
核心是传入一个自定义 io.Writer(其实只是用来接管响应输出)和一个错误处理函数,Gin 会在 recover 后自动调用它:
func CustomRecovery() gin.HandlerFunc {
return gin.RecoveryWithWriter(&customResponseWriter{}, func(c *gin.Context, err interface{}) {
stack := debug.Stack()
log.Printf("[PANIC] %v\n%s", err, stack)
// 生产环境脱敏,开发环境可加 stack 字段
resp := gin.H{
"code": 500,
"msg": "服务暂时不可用",
"data": nil,
}
c.AbortWithStatusJSON(500, resp)
})
}
// customResponseWriter 是个空实现,只为满足接口
type customResponseWriter struct{}
func (w *customResponseWriter) Write(p []byte) (n int, err error) { return 0, nil }
func (w *customResponseWriter) WriteHeader(int) {}
func (w *customResponseWriter) Header() http.Header { return http.Header{} }
- 必须用
c.AbortWithStatusJSON(),不能用c.JSON()+c.Abort(),否则可能触发http: multiple response.WriteHeader calls -
debug.Stack()只能在 recover 内安全调用,别在 handler 里提前取 - 注册时要放在
router.Use()最前面,确保它包裹所有其他中间件
异步 goroutine 的 panic 为什么捕不到
gin.Recovery() 只捕获 HTTP 请求主 goroutine 的 panic。你在 go func() {}() 里触发的 panic,Gin 根本看不见——这不是 bug,是 Go 并发模型的天然限制。
常见踩坑点:
- 在中间件或 handler 里启动 goroutine 后直接调用
c.JSON()—— 此时c已失效,必然 panic - 用
go func() { c.Error(err) }()想异步上报错误,但c生命周期已结束 - 第三方 SDK 内部启 goroutine 并 panic,你完全无感知
正确做法是:所有异步逻辑自己加 defer+recover,且只记录日志,绝不操作 c:
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("async panic in task: %v", r)
}
}()
// 纯业务逻辑,不碰 c、不写响应
}()
业务 error 和 panic 必须分开处理
panic 是程序崩溃信号,error 是可控的业务异常。混用会导致响应状态码错乱、错误字段不一致、前端无法区分客户端错误和服务端故障。
实操分界线很明确:
- 参数校验失败、数据库查不到、第三方返回 4xx —— 用
c.AbortWithError(400, err),立刻终止并返回结构化错误 - 空指针、数组越界、未初始化 map 写入 —— 属于 panic,由
CustomRecovery统一兜底为 500 - 别在 handler 里
panic(err),除非你明确想触发崩溃恢复流程
容易被忽略的一点:即使用了 c.AbortWithError(),也要确保它前面没有 c.JSON() 或 c.String() —— 否则响应体可能拼接出两个 JSON,HTTP 状态码和 body 对不上。


















