recover必须在defer函数中调用且需在panic前注册,HTTP中间件中需包裹next.ServeHTTP;子goroutine panic无法被主goroutine recover捕获,须各自加defer recover。

Go HTTP中间件里recover没捕获到panic?检查defer顺序
Go的recover必须在defer函数中调用,且该defer必须在panic发生前已注册。常见错误是把defer recover()写在handler内部、但又在中间件里提前return了,导致defer根本没执行。
正确做法是在中间件最外层注册defer,并在其中调用recover:
func PanicLogger(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC in %s %s: %+v\n%v", r.Method, r.URL.Path, err, debug.Stack())
}
}()
next.ServeHTTP(w, r)
})
}
- 必须用
defer func() { ... }(),不能写成defer recover()(会立即执行,返回nil) -
debug.Stack()比runtime.Stack()更易读,需导入"runtime/debug" - 这个
defer必须包裹整个next.ServeHTTP调用,否则panic发生在中间件之后就来不及捕获
为什么log.Printf输出的堆栈只有1行?记得加%+v或用debug.Stack()
直接fmt.Sprintf("%v", err)或log.Println(err)只会打印panic值本身(比如runtime error: index out of range),不带堆栈。真正要记录上下文,得显式触发栈捕获。
- 用
debug.Stack()获取完整goroutine堆栈,返回[]byte,转字符串后可直接写入日志 - 避免用
runtime.Caller手动拼接——它只返回单帧,无法还原panic时的调用链 - 如果用了结构化日志库(如
zerolog或zap),传入debug.Stack()结果时注意用String()方法或转string(),别直接当[]byte塞进去(可能被误解析)
panic发生在goroutine里怎么办?HTTP中间件捕获不到
HTTP handler里启动的子goroutine(比如go doSomething())发生的panic,**不会传播到主线程,中间件的recover完全无效**。这是最容易忽略的盲区。
立即学习“go语言免费学习笔记(深入)”;
- 所有异步逻辑必须自行
defer recover(),例如:go func() { defer func(){...}(); doSomething() }() - 数据库查询、定时任务、消息消费等后台操作,建议统一包装一个
SafeGo函数,内置recover和日志上报 - 不要依赖全局panic handler(如
signal.Notify捕获SIGQUIT)——它只能抓进程级崩溃,对goroutine panic无能为力
生产环境要不要把panic堆栈打到标准输出?看日志收集方式
本地开发时log.Printf够用,但生产环境通常走日志采集(如Filebeat→ES、Loki)。这时要注意:
- 避免混用
log.Print和fmt.Print——前者带时间戳,后者没有,会导致日志时间错乱 - 如果用
zap,推荐logger.Panic()而非logger.Error(),它会自动触发os.Exit(1),适合关键panic场景 - 堆栈过长(比如嵌套50层)可能被日志系统截断,可在
debug.Stack()后做简单截断:string(buf[:min(len(buf), 10240)])
真正难的不是捕获panic,而是确认所有goroutine都覆盖了recover——尤其是第三方库回调、context.WithCancel触发的清理函数、http.TimeoutHandler内部启动的goroutine,这些地方最容易漏。


















