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()
}
如上的 func1、func2、func3 调用链关系:
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()
}
调用链关系未变,但 func2、func3 在不同的 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)
}))