gin中panic默认不打印堆栈,需启用debugmode或自定义recovery中间件调用debug.printstack();中间件内panic堆栈常被裁剪,应在可疑处主动recover并打印;第三方库panic堆栈缺失上下文,需在handler顶层defer捕获;为关联请求应注入req_id并在panic日志中输出。

panic 发生时没打印堆栈,只看到 500 错误
默认情况下,Gin 的 Recovery() 中间件会捕获 panic 并返回 500,但不输出堆栈到控制台——这导致你根本不知道哪行代码崩了。关键不是关掉 Recovery,而是让它把 panic 信息打出来。
实操建议:
- 启动时用
gin.SetMode(gin.DebugMode),确保 Recovery 日志开启(生产环境慎用) - 手动替换默认 Recovery:在
engine.Use()前插入自定义中间件,调用debug.PrintStack()或fmt.Fprint(os.Stderr, debug.Stack()) - 注意:
Recovery()内部已调用debug.PrintStack(),但前提是日志输出未被重定向或静默;检查是否设置了gin.DefaultWriter = io.Discard这类操作
panic 在 middleware 里发生,但堆栈指向 handler 入口
Gin 的 handler 执行链是函数链式调用,panic 若发生在某个中间件中,Go 默认堆栈可能只显示最外层的 c.Next() 调用点,而不是真正出错的那行。这不是 Gin 的 bug,是 Go runtime 对 defer/panic 栈帧的裁剪行为。
实操建议:
- 在可疑中间件内加
defer func() { if r := recover(); r != nil { log.Printf("PANIC in %s: %+v", "AuthMiddleware", r) ; debug.PrintStack() } }(),主动捕获并打印 - 避免在中间件里写
panic(fmt.Errorf(...)),改用显式错误返回 +c.AbortWithStatusJSON(),把“异常流”转为“控制流” - 用
runtime.Caller(1)获取当前执行文件和行号,辅助定位(尤其适用于通用日志中间件)
第三方库触发 panic,但堆栈缺失关键帧
比如 json.Unmarshal() 遇到空指针、gorm.Model().Where().First() 对 nil struct 操作、或是模板渲染时 {{.Field.X}} 访问空指针——这些 panic 堆栈常止步于标准库或库内部,看不到你自己的调用上下文。
实操建议:
- 在顶层 handler 函数开头加
defer func() { if p := recover(); p != nil { log.Printf("[HANDLER PANIC] %s %s: %v", c.Request.Method, c.FullPath(), p) ; debug.PrintStack() } }() - 对高危操作做防御:如解包前判空
if len(body) == 0 { c.AbortWithStatusJSON(400, ...) };查库前确保 struct 地址非 nil - 启用 Go 的 panic stack trace 完整模式:启动时加环境变量
GOTRACEBACK=crash(开发机可用,CI/线上谨慎)
想让 panic 自动带上请求 ID 和路径信息
单纯看堆栈还不够,特别是并发请求多时,得知道这个 panic 是哪个请求触发的。Gin 本身不自动注入 request ID,需手动串联。
实操建议:
- 在第一个中间件(如日志中间件)生成唯一
reqID := uuid.New().String(),存入c.Set("req_id", reqID) - 自定义 Recovery 中间件,在 recover 后从
c.Get("req_id")取 ID,并打到日志里:log.Printf("[REQ:%s] PANIC: %+v\n%+v", reqID, p, debug.Stack()) - 注意:
c.Get()返回interface{},需类型断言;若 req_id 不存在,fallback 到c.Request.URL.Path和c.Request.Header.Get("X-Request-ID")
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











