子协程 panic 不会自动传播到主 goroutine,必须在每个子协程内用 defer + recover 显式捕获;http 中间件的 recover 仅作用于当前 handler goroutine,无法捕获其启动的子协程 panic。

子协程 panic 不会自动传播到主 goroutine
Go 的 panic 是 goroutine 局部的,一旦在子协程中发生,除非显式处理,否则它只会终止该 goroutine,并**不会向上冒泡**,主 goroutine 完全感知不到——这和主线程里抛异常完全不同。很多开发者误以为用 recover 包一层就能“兜住”所有 panic,结果线上服务悄无声息地丢掉子任务,日志里连痕迹都没有。
常见错误现象:go func() { panic("oops") }() 执行后程序继续跑,但子协程已退出,无报错、无日志、无监控告警。
- 必须在子协程内部调用
recover,且要放在defer中(否则执行不到) -
recover只在defer函数里且 panic 正在发生时才有效;放在普通函数里直接返回nil - 不要依赖外部
recover中间件“统一捕获”子协程 panic——它根本收不到
正确写法:每个 go routine 自带 defer + recover
没有银弹,只能在启动子协程的地方主动防御。这不是冗余,而是 Go 并发模型决定的约束。
示例:
立即学习“go语言免费学习笔记(深入)”;
go func() {
defer func() {
if r := recover(); r != nil {
log.Printf("goroutine panicked: %v", r)
// 这里可上报监控、发告警、写 trace ID
}
}()
// 你的业务逻辑
doSomethingThatMightPanic()
}()
- 务必把
defer写在子协程最开头,避免逻辑提前 return 跳过它 -
recover()返回值类型是interface{},需做类型断言或直接格式化输出,别只打印%v就完事 - 如果子协程需要向主 goroutine 传递 panic 状态(比如 worker pool 中某个任务失败),得靠 channel 或 error 回传,不能靠 panic 传播
HTTP handler 中的 recover 中间件只管本 goroutine
很多人写了个 recoveryMiddleware,以为能兜住 handler 里启的子协程 panic,其实不能。HTTP server 启动一个新 goroutine 处理每个请求,中间件的 defer recover 只对这个 handler goroutine 有效。
典型误用场景:
func recoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if r := recover(); r != nil {
http.Error(w, "Internal Error", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r) // 如果这里 go func(){ panic() },中间件完全不知道
})
}
- 中间件里的
recover和子协程之间没有调用链关系,无法拦截 - 若 handler 内部启子协程做异步操作(如发消息、写日志、调下游),这些子协程 panic 必须各自处理
- 想统一管理?可以封装一个
SafeGo工具函数,内部自带defer recover和日志/监控回调,强制团队使用
recover 失效的几个隐蔽坑
recover 看似简单,但在并发场景下容易失效,不是代码写错了,而是时机或位置不对。
- 子协程还没来得及执行
defer就被系统 kill(如 OOM、syscall.SIGKILL)——recover根本没机会运行 - panic 发生在
init函数或包加载阶段,此时还没有 goroutine 上下文,recover无效 - 用了
runtime.Goexit()终止 goroutine,这不是 panic,recover捕不到 - 在 defer 函数里又 panic 了一次,且没再 recover——会导致进程级 crash,尤其在测试中容易漏掉
真正难的不是写 recover,而是判断哪些 goroutine 值得加、加在哪、panic 后要不要重试、要不要降级、要不要通知上游——这些没法靠语法糖解决。










