594 words
3 minutes
记一个子 goroutine 导致服务崩溃的坑
2025-04-27

背景#

最近在微服务开发中遇到一个非常危险的问题。 我们使用 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 向上溢出,最终直接导致程序崩溃。

解决方案#

  1. 截至目前(2025年4月28日),最新的errgroup依然没有处理函数中的 panic,但我认为在 handler 中启动新 goroutine 时,依然应该通过 errgroup 或其他机制进行统一管理。这样可以简单地为每个任务自动封装 recover,保证 panic 被捕获并优雅地处理。

  2. 目前官方的 errgroup 已经在 master 分支修复了 goroutine 内 panic 的问题,不过还未正式发布。

  3. 在此之前,你可以参考我写的 safegroup,它为每个子任务自动添加 recover 包装,并提供了一种类型安全的方式以处理 panic 错误。

记一个子 goroutine 导致服务崩溃的坑
https://akishichinibu.github.io/posts/20250427errgroup/
Author
Axitinib
Published at
2025-04-27