默认 gin.recovery() 不打印堆栈因 debug.printstack() 输出到 os.stderr,易被 gin.defaultwriter = io.discard 等设置屏蔽;需在 handler 顶层、中间件内、goroutine 入口三处显式 recover 并用 debug.stack() 写入统一日志器。

默认的 gin.Recovery() 中间件能防止服务崩溃,但不输出堆栈、不记录请求上下文、不区分 panic 类型——你看到的只是 500 Internal Server Error 和一行日志,根本不知道哪行代码崩了。
为什么默认 Recovery 不打印堆栈
因为 gin.Recovery() 内部调用的是 debug.PrintStack(),而该函数输出到 os.Stderr;如果项目中设置了 gin.DefaultWriter = io.Discard(常见于日志统一采集场景),或重定向了标准错误流,堆栈就彻底消失了。
- 检查是否误设了
gin.DefaultWriter或gin.DefaultErrorWriter - 生产环境别依赖
gin.SetMode(gin.DebugMode),它只控制日志格式,不保证堆栈可见 - 真正可靠的做法:在自定义中间件里显式调用
debug.Stack(),然后写入你自己的日志器(如zap、logrus)
中间件里 panic 堆栈只显示 c.Next() 怎么办
Gin 的 handler 执行链是 c.Next() 触发的函数调用链,Go runtime 在 panic 时对 defer 栈帧做了裁剪,导致堆栈顶部总是 c.Next(),而不是你写的业务逻辑行号。
- 在可疑中间件内部加独立
defer func() { if r := recover(); r != nil { log.Printf("PANIC in AuthMiddleware: %v", r); debug.PrintStack() } }() - 避免在中间件里主动
panic(fmt.Errorf(...)),改用c.AbortWithStatusJSON(401, ...)显式返回错误 - 需要精确定位时,在中间件开头加
_, file, line, _ := runtime.Caller(1)打点日志
第三方库 panic 没有你的代码帧
比如 json.Unmarshal(nil, &v)、template.Execute(nil, data)、gorm.First(&u, 0) 对空 struct 操作——这些 panic 发生在标准库或 gorm 内部,堆栈直接跳进 reflect 或 encoding/json,你的 handler 入口完全不出现。
- 必须在每个 handler 函数最开头加顶层
defer:defer func() { if p := recover(); p != nil { log.Printf("[HANDLER PANIC] %s %s: %v", c.Request.Method, c.FullPath(), p); debug.PrintStack() } }() - 对高危调用做防御:解包前判空、查库前校验 ID、模板渲染前确保 data 非 nil
- 给 panic 值带上上下文,例如
panic(fmt.Sprintf("json unmarshal failed for %s: %v", c.FullPath(), err))
goroutine 里的 panic 为什么 recover 不到
recover() 只对**当前 goroutine** 有效。你在 handler 里写了 defer recover,但里面起的 go func() { panic("x") }() 是另一个 goroutine,主 goroutine 的 recover 完全无效——这个 panic 会被 Go 运行时静默吞掉,或者触发进程级崩溃(取决于是否启用了 debug.SetPanicOnFault)。
- 所有裸
go fn()必须包装成safeGo(func() { ... }),内部自带defer recover - 别信“全局 panic 捕获”——
signal.Notify或debug.SetPanicOnFault(true)对绝大多数业务 panic(空指针、切片越界、map 写入)完全无效 - main 函数开头也可以加
defer recover,捕获 init 阶段或 main goroutine 的 panic,但这和 HTTP 请求无关
真正难的不是写一个 recover,而是意识到它只是一层薄薄的纱——goroutine 分层、中间件嵌套、第三方库黑盒、日志管道截断……每一层都可能让 panic 消失得无影无踪。最稳妥的方式是:handler 顶层 + 中间件内 + goroutine 入口,三处都放上带堆栈的日志,再靠 req_id 串联起来。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











