Gin 的 panic 会直接崩溃是因为默认不 recover 路由 handler 中的 panic,需显式使用 gin.Recovery() 中间件捕获并返回 500;gin.Default() 已内置该中间件,而 gin.New() 需手动注册,且它仅捕获同步调用链中的 panic。

为什么 Gin 的 panic 会直接崩溃而不是返回 500?
Gin 默认不 recover 路由处理函数中的 panic,一旦 handler 内部触发 panic(比如空指针解引用、切片越界),Go 运行时会直接终止当前 goroutine,而 Gin 没有兜底逻辑,导致连接中断、无响应、日志缺失——你看到的往往是客户端超时或 Nginx 返回 502。
用 gin.Recovery() 中间件是最简方案
它本质是调用 recover() 捕获 panic,记录错误,并返回 500 响应。这是 Gin 官方推荐且开箱即用的方式:
func main() {
r := gin.Default() // 默认已包含 Recovery()
// 或显式注册:
// r := gin.New()
// r.Use(gin.Recovery())
r.GET("/panic", func(c *gin.Context) {
panic("boom")
})
r.Run(":8080")
}
-
gin.Default()已内置gin.Logger()和gin.Recovery(),多数场景直接用它即可 - 若用
gin.New(),必须手动r.Use(gin.Recovery()),否则 panic 仍会逃逸 - 该中间件默认将 panic 错误写入标准日志(含堆栈),但不会暴露给客户端——响应体为空,状态码为 500
想自定义 panic 处理逻辑?替换 gin.RecoveryWithWriter()
原生 Recovery() 不支持修改响应内容或状态码。你需要传入自定义 writer 和错误处理函数:
开箱即用的技能链路由引擎。13 条预定义链覆盖搜索、开发、审查、MLOps、法律、创意等场景,三层路由架构(触发词→SAD反馈→DAG编排),recall@10=96.97%。配置驱动(chains.yaml),零代码扩展。pip install skill-weave-chains 一键安装。
r.Use(gin.RecoveryWithWriter(
gin.DefaultErrorWriter,
func(c *gin.Context, err interface{}) {
// 记录结构化日志(如 sentry、zap)
log.Printf("[PANIC] %v\n%v", err, debug.Stack())
// 返回 JSON 错误(注意:c.Writer 已写过 header,不能再设 Status)
c.AbortWithStatusJSON(500, gin.H{
"error": "internal server error",
"trace_id": uuid.New().String(),
})
},
))
- 务必在
c.AbortWithStatusJSON()前调用c.Abort()或确保没其他中间件已写响应体,否则会 panic:“write after write” -
debug.Stack()开销较大,生产环境建议只在 debug 模式启用,或用更轻量的runtime.Caller()提取关键位置 - 如果用了自定义 logger(如 zap),别直接依赖
gin.DefaultErrorWriter,应把错误转给你的 logger 实例
哪些 panic 容易被漏掉?
gin.Recovery 只捕获路由 handler 及其同步调用链中的 panic。以下情况它无能为力:
立即学习“go语言免费学习笔记(深入)”;
- goroutine 内部 panic(如
go func(){ panic(...) }())——需在 goroutine 内部单独 recover - HTTP Server 启动阶段 panic(如
flag.Parse()失败、配置加载异常)——发生在 Gin router 初始化之前,必须靠外层defer/recover包裹main() - 中间件自身 panic 且未被上层 recovery 拦截(比如你在
Recovery()之前注册了另一个 panic-prone 中间件)——顺序很重要,Recovery()必须是第一个或至少在所有可能 panic 的中间件之下
真正健壮的服务,recover 是分层的:全局启动保护 + Gin 路由 recover + 关键异步任务内部 recover。


















