gin 默认 panic 恢复仅打印堆栈并返回 500,生产环境会暴露敏感信息且无法统一日志、区分错误类型或返回结构化响应;需用 gin.new() 替换默认 recovery 中间件,自定义 mypanicrecovery() 实现 panic 分类处理、结构化响应与完整日志上报。

为什么 Gin 的默认 panic 恢复行为不够用
Gin 默认会在 Recovery() 中间件里 recover panic,但只做两件事:打印堆栈、返回 500。这在生产环境会暴露敏感信息(比如路径、变量值),且无法统一记录日志、无法区分业务 panic 和系统 panic、也无法按需返回结构化错误响应。
如何替换默认 Recovery 中间件并自定义 panic 处理逻辑
直接调用 gin.Default() 会自动加载 Recovery(),所以得用 gin.New() 手动组装中间件链,并把自定义 panic 捕获逻辑放在最后(确保它能捕获其他中间件抛出的 panic):
router := gin.New() router.Use(gin.Logger()) router.Use(MyPanicRecovery()) // 替换默认 Recovery router.Use(MyAuthMiddleware()) // ... 其他中间件
关键点:
-
MyPanicRecovery()必须是中间件函数,内部用defer+recover()捕获 - 不要在中间件里直接调用
c.Abort()后就 return —— 要显式调用c.AbortWithStatusJSON()或c.JSON()并设状态码 - panic 值可能是
string、error或任意类型,建议统一转成fmt.Sprintf("%v", err)再处理
如何区分 panic 类型并做差异化响应
不是所有 panic 都该返回 500。比如你主动 panic(errors.New("invalid token")),其实想表达的是客户端错误,应返回 401;而空指针解引用这种才该归为服务端错误。常见做法:
定义一个带错误码的 panic 类型:
type BusinessError struct {
Code int `json:"code"`
Msg string `json:"msg"`
}
func (e BusinessError) Error() string { return e.Msg }
// 然后在 handler 里:
panic(BusinessError{Code: 401, Msg: "token expired"})
在 MyPanicRecovery() 中判断:
- 如果
err是BusinessError类型,取其Code作为 HTTP 状态码,Msg作为响应体 - 如果是
error接口但非自定义类型,视为客户端错误(如 JSON 解析失败),返回 400 - 其他情况(
nil、string、未识别类型)一律记日志 + 返回 500
日志记录和 Sentry 上报要注意什么
panic 日志必须包含:c.Request.URL.Path、c.Request.Method、c.ClientIP()、完整堆栈(用 debug.Stack())、以及 panic 值本身。别只打 err.Error() —— 它可能为空或无意义。
Sentry 上报时:
- 避免在 defer 里直接调用
sentry.CaptureException()—— 此时 goroutine 可能已开始清理,上下文丢失 - 推荐用
sentry.WithScope()包一层,手动设置 tags(如path、method)和 extra(如panic_value、stack) - 生产环境务必关闭
sentry.Flush()的阻塞等待,改用异步发送(Sentry SDK 默认就是异步)
真正难的不是捕获 panic,而是让 recover 后的状态可预测:HTTP 状态码不乱、日志字段不丢、监控指标能区分故障类型。这些细节不写进中间件,上线后就会在凌晨三点给你打电话。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











