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