gin.Recovery() 会捕获空指针 panic,但仅打印日志并返回空白 500 页面,不执行自定义错误响应逻辑;要返回结构化 JSON,需自定义 recovery 中间件,用 defer+recover+c.AbortWithStatusJSON(),且 panic 必须发生在 c.Next() 调用链内。

空指针 panic 不会被 Gin 默认的 gin.Recovery() 拦住——它确实会捕获,但只打印日志、返回空白 500 页面,不走你写的错误响应逻辑。真要捕获并返回结构化 JSON,必须自己写 recovery 中间件,且确保 panic 发生在 c.Next() 调用链内。
为什么 gin.Recovery() 看起来没起作用
默认中间件只做两件事:recover 当前 goroutine 的 panic,然后调用 log.Print 打印堆栈,最后直接返回空响应体 + 500 状态码。它根本不会调用 c.AbortWithStatusJSON(),也不触发你注册的其他错误处理中间件。
- 如果你看到的是纯白页或 curl 返回空 body,基本就是默认 recovery 在干活
- 它不检查
c.Errors,不读取业务 error,也不管你定义的统一响应结构 - panic 发生在
c.Next()之前(比如中间件里解包 nil 指针),那 recovery 根本没机会运行
怎么写一个真正能返回 JSON 的 recovery 中间件
核心是 defer + recover + c.AbortWithStatusJSON(),且必须放在所有前置中间件之后、c.Next() 之前。
- 别用
c.JSON(500, ...)+c.Abort()组合——万一后续中间件又写了响应,会 panic 或状态码错乱 - 务必用
c.AbortWithStatusJSON(),它内部自动调用c.Abort()并终止整个中间件链 - 对
recover()返回值做类型判断,避免panic(nil)或字符串 panic 导致断言失败 - 生产环境禁止直接输出
err.Error(),统一返回脱敏消息,比如"internal server error"
func SafeRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if r := recover(); r != nil {
var msg string
switch v := r.(type) {
case string:
msg = "internal server error"
case error:
msg = "internal server error"
default:
msg = "internal server error"
}
c.AbortWithStatusJSON(500, gin.H{
"code": 500,
"msg": msg,
"data": nil,
})
}
}()
c.Next()
}
}
空指针 panic 容易发生在哪些位置
不是所有 nil 解引用都会被这个 recovery 捕获——它只管 c.Next() 及之后的 handler 和中间件。以下位置的 panic 会逃逸:
立即学习“go语言免费学习笔记(深入)”;
- 在
c.Next()前手动解包:比如user := ctx.Value("user").(*User); user.Name,而ctx.Value("user")是 nil - 在
router.NoRoute()或router.NoMethod()里写逻辑,这些 handler 不经过主中间件链 - 在
go func() { ... }()里触发 panic,子 goroutine 的 panic 不会被主 goroutine 的 defer 捕获 - 调用第三方库时传入 nil 参数(如
json.Unmarshal(nil, &v)),且该调用不在c.Next()调用链中
如何提前规避空指针,而不是靠 recover 补救
recover 是兜底,不是替代防御性编程。真正稳定的写法是主动检查:
- 从
c.MustGet()或c.Get()取值后,先判空再断言:if u, ok := c.Get("user").(*User); !ok || u == nil { ... } - 用
errors.Is(err, xxx)替代err == xxx,避免 nil error 比较 panic - 结构体字段加
json:",omitempty"或用指针字段(如*string)显式表达可空性 - 在
ShouldBind后不要直接用req.Field,先确认req != nil
最危险的其实是“看起来安全”的地方:比如 user.Name 前没检查 user 是否为 nil,这种 panic 一旦发生,recover 虽能兜住,但说明代码已经越过第一道防线了。


















