背景
最近在微服务开发中遇到一个非常危险的问题。 我们使用 echo
框架,在 handler
中通过 errgroup
启动 goroutine,结果 goroutine 内发生 panic。即使我们添加了 Recover
中间件来保护服务进程,服务依然直接崩溃,并导致其他请求同一服务器的用户请求被中止。
这暴露了一个很常见但容易忽略的坑:
defer recover
无法处理子goroutine的panic
问题复现
你可以运行下面的代码
package main
import (
"context"
"fmt"
"golang.org/x/sync/errgroup"
)
func TestPanicRecovered() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
panic("panic")
}
func TestPanicRecoverFailed() {
defer func() {
if r := recover(); r != nil {
fmt.Println(r)
}
}()
g, _ := errgroup.WithContext(context.TODO())
g.Go(func() error {
panic("panic")
})
err := g.Wait()
fmt.Println(err)
}
func main() {
TestPanicRecovered()
TestPanicRecoverFailed()
}
在 TestPanicRecovered
中,defer-recover 能正常捕获 panic。 但在 TestPanicRecoverFailed
中,即使在主 goroutine 使用了 recover
,子 goroutine 中的 panic
仍然直接导致崩溃。
为什么 Echo 的 Recover 中间件无效?
看一下 Echo 的 Recover 中间件实现(参考源码):
// RecoverWithConfig returns a Recover middleware with config.
// See: `Recover()`.
func RecoverWithConfig(config RecoverConfig) echo.MiddlewareFunc {
// Defaults
if config.Skipper == nil {
config.Skipper = DefaultRecoverConfig.Skipper
}
if config.StackSize == 0 {
config.StackSize = DefaultRecoverConfig.StackSize
}
return func(next echo.HandlerFunc) echo.HandlerFunc {
return func(c echo.Context) (returnErr error) {
if config.Skipper(c) {
return next(c)
}
defer func() {
if r := recover(); r != nil {
if r == http.ErrAbortHandler {
panic(r)
}
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
}
...
可以发现,它只捕获了当前 HTTP 请求 goroutine 中的 panic
。子 goroutine 产生的 panic
,无法通过中间件拦截。
根据 Go 官方文档:
The process continues up the stack until all functions in the current goroutine have returned, at which point the program crashes.
panic 只能在发生的 goroutine 栈内被 recover, 否则,panic 向上溢出,最终直接导致程序崩溃。
解决方案
截至目前(2025年4月28日),最新的
errgroup
依然没有处理函数中的panic
,但我认为在 handler 中启动新 goroutine 时,依然应该通过errgroup
或其他机制进行统一管理。这样可以简单地为每个任务自动封装 recover,保证 panic 被捕获并优雅地处理。目前官方的 errgroup 已经在 master 分支修复了 goroutine 内 panic 的问题,不过还未正式发布。
在此之前,你可以参考我写的 safegroup,它为每个子任务自动添加 recover 包装,并提供了一种类型安全的方式以处理
panic
错误。