默认的gin recovery不够“优雅”是因为它仅打印简略panic日志、不记录完整堆栈、不保留请求上下文(如trace id、path、method)、不支持结构化上报,且无法区分业务panic与系统崩溃;需自定义中间件,用debug.stack()捕获完整堆栈,结合zap等日志库结构化记录,并通过类型断言实现panic分类处理与动态http状态码返回。

为什么默认的Gin panic recovery 不够“优雅”
Gin 内置的 Recovery() 中间件确实会捕获 panic 并返回 500,但它把原始 panic 信息全丢进日志,不记录堆栈、不区分业务 panic 和系统崩溃、也不支持自定义上报逻辑。线上出问题时你只看到一行 panic: runtime error: invalid memory address,没上下文、没 trace ID、没调用链,根本没法快速定位。
如何替换默认 Recovery 中间件并保留完整堆栈
核心是自己写一个中间件替代 Recovery(),在 recover 后手动提取 panic 值和 stack trace,并交由结构化日志库(如 zap)记录。关键点在于:必须用 debug.Stack() 获取当前 goroutine 的完整堆栈,而不是只打印 panic value;且要确保在 defer 中调用 recover,否则会漏掉。
- 不要直接用
recover()返回的 interface{} 做字符串拼接——它可能只是"nil pointer dereference",没堆栈 - 必须在 defer 函数里调用
recover(),且 defer 要紧贴 handler 函数开头,不能包在其他闭包里 - 推荐用
runtime/debug.Stack()而非runtime.Caller()链式收集——后者只给顶层几帧,不够定位
func CustomRecovery() gin.HandlerFunc {
return func(c *gin.Context) {
defer func() {
if err := recover(); err != nil {
stack := debug.Stack()
zap.L().Error("panic recovered",
zap.String("path", c.Request.URL.Path),
zap.String("method", c.Request.Method),
zap.Any("panic", err),
zap.ByteString("stack", stack),
)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": "internal server error"})
}
}()
c.Next()
}
}
怎么区分业务 panic 和意外 panic
很多团队会用自定义 error 类型(比如 BusinessError)或 panic 值类型来标记“可控 panic”,比如权限校验失败主动 panic。这时候不能一概 500,而应转为对应状态码(如 403)并透出提示。关键在 panic 值的类型判断,而非字符串匹配。
使用 uber-go/dig 在 Golang 中实现依赖注入——基于反射容器,提供 Provide/Invoke、dig.In/dig.Out 参数与结果对象,支持命名依赖等
- 定义一个空接口
type BusinessPanic interface{ IsBusinessPanic() },让业务 panic 类型实现它 - 在 recover 后用
if bp, ok := err.(BusinessPanic)判断,ok 为 true 就走业务错误流程 - 避免用
strings.Contains(fmt.Sprintf("%v", err), "permission")这种脆弱方式——字段名一改就失效
HTTP 状态码和响应体要不要随 panic 类型动态变化
要,但得收敛规则。比如所有实现了 HTTPStatus() int 方法的 panic 类型,就取它的返回值;否则统一 500。这样既灵活又不至于每个 panic 都写 if 分支。
- panic 值本身可以带状态码和 message 字段,比如
panic(&HTTPError{Code: 400, Msg: "invalid input"}) - 中间件里用 type assertion 提取:
if he, ok := err.(*HTTPError); ok { c.AbortWithStatusJSON(he.Code, gin.H{"error": he.Msg}) } - 注意:不要在 panic 里传指针到局部变量(比如
panic(&err)),goroutine 结束后内存可能已释放
真正难的是 panic 发生在第三方库或中间件内部时,你无法控制 panic 值类型。这时候堆栈和请求上下文(trace id、user id)就是唯一救命稻草——别省日志字段。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










