必须在中间件最外层c.next()前用defer+recover捕获panic,记录完整堆栈并返回500响应;recover仅对同一goroutine中c.next()前注册的defer有效,且须主动写响应、带请求上下文日志,goroutine内panic需单独封装safego处理。

中间件里 defer + recover 必须写在 c.Next() 前
Go 的 http.ServeHTTP 不会自动 recover panic,一旦 handler panic,连接直接断开,还可能泄露堆栈。中间件必须显式捕获——但位置错了就白写。
常见错误是把 defer func() { recover() }() 放在 c.Next() 后面,或者包在 if/else 分支里。recover 只对**同一函数内、panic 发生前已注册的 defer** 有效。
-
c.Next()是实际执行业务 handler 的地方,panic 一定发生在这里或其调用链中 - recover 必须在
c.Next()调用前声明,且不能被条件语句包裹(比如写在if c.Request.URL.Path != "/health" {}里) - 必须调用
c.Abort(),否则 panic 后中间件退出,后续中间件或 handler 仍可能执行(比如 JWT 验证逻辑继续跑)
panic 日志必须含 stack trace,不能只打 r.Error() 或 r
只写 log.Printf("panic: %v", r) 会丢失调用栈,你根本不知道 panic 发生在哪一行、哪个 goroutine。线上排查时等于盲人摸象。
runtime/debug.Stack() 才是正确姿势,它返回完整的 goroutine stack trace 字符串,包含文件名、行号、函数名。
- 别漏 import
"runtime/debug" - 生产环境可截断过长 stack(比如取前 2000 字节),但不能省略
- 日志里必须带上请求上下文:method、path、traceID(从
c.Request.Context()提取),否则无法关联请求
goroutine 内 panic 不会被中间件捕获,必须单独加 safeGo
中间件的 recover 只管主请求 goroutine。只要你在 handler 里写了 go sendMQ(msg) 或 go cleanup(),里面 panic 就完全逃逸——主响应照常返回,MQ 消息丢了,日志里连个影子都没有。
没有“全局” recover,只有分层防御:
- 所有显式启动的 goroutine 必须自带
defer func() { if r := recover(); r != nil { /* log with stack */ } }() - 禁止裸写
go fn(),统一走封装好的safeGo()函数 - 第三方库回调(如 prometheus 的 metric collect)、定时器
time.AfterFunc、channel 处理循环,全都要自己加 recover
别用 debug.SetPanicOnFault(true) 当全局兜底
这个名字极具误导性。它只对极少数底层内存违规(如 nil pointer dereference 在某些平台触发 SIGSEGV)生效,且仅限 Linux/AMD64,默认关闭。
对 99% 的业务 panic 完全无效:panic("xxx")、空 map 写入、切片越界、channel 关闭后发送……全都不触发。
- 别把它当 recover 替代品,它和 HTTP 错误处理毫无关系
- 开启后反而可能掩盖真实问题,比如让本该快速失败的 panic 变成进程级信号终止
- 真正兜底靠的是:handler 中间件 + goroutine 封装 + main 函数开头的 defer recover(防 init 或 main goroutine 崩溃)
最常被忽略的是:recover 后没写响应头和 body,导致客户端卡在等待状态,直到超时;还有就是把业务 error 和系统 panic 混在一起处理,结果参数校验失败(400)也被当成 500 上报。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











