bigywater.tech
Gin · 56

中间件里起的 goroutine 发生 panic:Recovery 管不管

在 handler 或中间件里 go func() {...}() 起一个 goroutine,里面 panic 了,Recovery 能接住吗?这篇起真实的服务进程,用 curl 请求,看进程是否存活、退出码、其它在途请求的命运,最后给出一个可用的 safeGo 写法。

退出码 2
goroutine 里 panic 时整个进程的退出码——Recovery 没有拦住
0 / 1
另一个请求触发 goroutine panic 时,同时在途的 /slow 请求的成功数(handler panic 时是 1 / 1)
存活
用 safeGo(goroutine 里 recover)后,panic 被捕获,服务继续处理请求

Recovery 的作用域

实测 · 本机执行输出
  GET /direct    → [HTTP 500](耗时 13 ms);之后:仍在运行(由我终止);再请求 /ok:ok [HTTP 200]
  GET /go        → [HTTP 000]curl: (52) Empty reply from server(耗时 23 ms);之后:进程已退出,退出码 2;再请求 /ok:(进程已退出,连接被拒绝)
  GET /go-after  → 已返回 [HTTP 200](耗时 20 ms);之后:进程已退出,退出码 2;再请求 /ok:(进程已退出,连接被拒绝)
  GET /guard     → goroutine panic 已被捕获:goroutine 里的 panic,自己 recover [HTTP 500](耗时 15 ms);之后:仍在运行(由我终止);再请求 /ok:ok [HTTP 200]

Recovery 是一个 defer + recover 中间件,只能捕获同一个 goroutine 里的 panic。handler 里直接 panic 会被接住,返回 500 并继续服务;handler 里起的 goroutine 是另一条调用栈,它的 panic 没有任何人接,Go 运行时直接终止整个进程。

崩溃长什么样

实测 · 本机执行输出
    panic: handler 返回之后 goroutine 才 panic
    goroutine 23 [running]:
    main.main.func5.1()
    created by main.main.func5 in goroutine 21
    进程已退出,退出码 2

进程输出 panic: …、goroutine N [running] 和栈帧,然后退出码 2;没有 Gin 的 [Recovery] 日志。最有用的线索是 created by … 一行,它告诉你这个 goroutine 是在哪里启动的。即使 handler 早已返回,后台 goroutine 的 panic 也会同样崩溃,这时已经没有请求上下文可以排查。

代价是全局的

实测 · 本机执行输出
  触发 /go-after  → 同时在途的 /slow:[HTTP 000]curl: (52) Empty reply from server;进程已退出,退出码 2
  触发 /direct    → 同时在途的 /slow:slow done [HTTP 200];仍在运行(由我终止)

一个 goroutine 的 panic 会让所有正在处理的请求同时失败(连接被切断),而不是只影响出错的那个。生产环境里这是比 handler 里 panic 严重得多的故障。

写法

实测 · 本机执行输出
  GET /safego        → 后台任务失败:assignment to entry in nil map [HTTP 500];之后再请求 /ok:ok [HTTP 200];仍在运行(由我终止)
      进程日志:捕获 panic: assignment to entry in nil map
  GET /safego-after  → 已返回 [HTTP 200];之后再请求 /ok:ok [HTTP 200];仍在运行(由我终止)
      进程日志:捕获 panic: handler 已返回,后台任务 panic

每个自己起的 goroutine 开头都应该 defer func() { if e := recover(); e != nil { ... } }()。可以封装成 safeGo:捕获后记录日志(带 debug.Stack())并通知调用方;如果 handler 还在等结果,通过 channel 把错误交回 handler 来写响应;如果 handler 已经返回,就只记日志、上报监控,不要再碰已经结束的 *gin.Context(需要用 c.Copy(),见前面的文章)。

分步演示

goroutine 里的 panic
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Go 1.27.1(brew)/ Gin v1.12.0(go.mod 里 require,源码在模块缓存里);用 go run / go test 直接编译运行,需要库时经代理从 proxy.golang.org 下载;HTTP 实验用 httptest 或本机回环端口(9700 以上);计时用 time.Now / testing.B,数字每次运行会有小幅抖动。服务端是编译好的真实二进制(gin.Default(),端口 9772 起),客户端是 curl;进程是否存活用 Python 的 Popen.poll,退出码来自进程;栈输出中的地址被整理成 (…);每种情形各一次
    • 没有验证:errgroup、sync.WaitGroup.Go 等标准写法下的 panic 传播;recover 之后是否应该继续运行(是否有损坏状态,取决于业务);runtime.Goexit;cgo 与信号导致的崩溃;Kubernetes 重启对服务可用性的影响;Gin 的 gin-contrib 里是否有现成的 safe goroutine 工具
    京ICP备2026067502号-1京公网安备11010502063487号