1分钟学会go语言中panic处理套路

1,325 阅读2分钟

go 提供一种 panic-recover 模式用于处理程序的异常,这和其他语言的采用的 Exception 机制类似。但也不太一样,使用时需要小心。

核心点:panic 是基于goroutine级别的异常机制,我们只要理解了这句话的思想,就能很好的使用它。

即是说,如果你程序里的 panic 的产生跨多个 goroutine 时,那么此时你想 catch 住该 panic ,一定在 panic 产生的同一个 goroutine 里进行捕获,否则无效。

同一协程内

如果 panic 都是在同一个goroutine里,那么直接可以在根函数进行捕获即可:

func func3() {
   panic("func3 panic")
}

func func2() {
   func3()
}

func func1() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
      }
   }()

   func2()
}

如上的 func1func2func3 调用链关系:

func1
  └── func2
        └── func3

由于三个函数都在同一个协程中,所以我们只要在根函数 func1 中即可捕获所有的后代子函数产生的 panic

不同协程

这种情况尤为要注意,否则容易掉坑。还是上面的例子:

func func3() {
   panic("func3 panic")
}

func func2() {
   go func3()
}

func func1() {
   defer func() {
      if err := recover(); err != nil {
         fmt.Println(err)
      }
   }()

   go func2()
}

调用链关系未变,但 func2func3 在不同的 goroutine 中,那么这时候在 func1 中是无法捕获到后续子协程的 panic 异常,而且,这种子协程的 panic 如果未有效拦截,也会导致主进程崩溃,所以不得不重视它。

所以,但凡你要新启一个 goroutine,而你又不完全确定是否它会产生 panic ,那么保险的做法在启动的时候进行拦截,做到 "谁创建,谁负责" 的原则。

func func3() {
   panic("func3 panic")
}

func func2() {
   go func() {
      defer func() {
         if err := recover(); err != nil {
            fmt.Println(err)
         }
      }()
      
      func3()
   }()
}

func func1() {
   go func() {
      defer func() {
         if err := recover(); err != nil {
            fmt.Println(err)
         }
      }()
      
      func2()
   }()
}

如此,我们便能处理好每一个 goroutine 所产生的异常问题。

封装

其实前面的总结,我们已经掌握了 panic 的处理原则,但是针对新启 goroutine 时的异常处理,代码相当冗余,所以我们打算封装一个启动新协成的函数,抛弃 go func 原生的启动方式(完整源文件:cxy.im/s.40tCUop ):

func Go(f func(), opts ...Option) {
   g := &gr{}
   for _, opt := range opts {
      opt(g)
   }

   go func() {
      defer func() {
         if err := recover(); err != nil {
            if g.errCallback != nil {
               g.errCallback(err)
            }
         }
      }()

      f()
   }()
}

使用

我们使用 Go() 替代官方的提供的 go func() 函数:

// 基本使用
r.Go(func() {
    // some code
})

// 带 panic err 回调函数
r.Go(func() {
    // some code
}, r.WithErrCallbackOpt(func(err any) {
   // 记录 panic err 错误
    app.Log().Err(err)
}))