引言
在Go语言中,咱们通常会用到panic和recover来抛出谬误和捕捉谬误,这一对操作在单协程环境下咱们失常用就好了,并不会踩到什么坑。然而在多协程并发环境下,咱们经常会碰到以下两个问题。假如咱们当初有2个协程,咱们叫它们协程A和B好了:
- 如果协程A产生了panic,协程B是否会因为协程A的panic而挂掉?
- 如果协程A产生了panic,协程B是否能用recover捕捉到协程A的panic?
答案别离是:会、不能。
那么上面咱们来一一验证,并给出在具体的业务场景下的最佳实际。
问题一
- 如果协程A产生了panic,协程B是否会因为协程A的panic而挂掉?
为了验证这个问题,咱们写一段程序:
package mainimport ( "fmt" "time")func main() { // 协程A go func() { for { fmt.Println("goroutine1_print") } }() // 协程B go func() { time.Sleep(1 * time.Second) panic("goroutine2_panic") }() time.Sleep(2 * time.Second)}
首先主协程开启两个子协程A和B,A协程不停的循环打印goroutine1_print字符串;B协程在睡眠1s后,就会抛出panic(睡眠这一步为了确保在A跑起来开始打印了之后,B才会panic),主协程睡眠2s,期待A、B子协程全副执行结束,主协程退出。最终打印后果如下:
...goroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printgoroutine1_printpanic: goroutine2_panicgoroutine1_printgoroutine1_printgoroutine goroutine1_print19goroutine1_printgoroutine1_printgoroutine1_printgoroutine1_print [runninggoroutine1_print]:goroutine1_printgoroutine1_printgoroutine1_printmain.main.func2() /Users/jiangbaiyan/go/src/awesomeProject/main.go:18 +0x46created by main.main /Users/jiangbaiyan/go/src/awesomeProject/main.go:16 +0x4d
咱们能够看到,在协程B产生panic之前,协程A始终在打印字符串;而后协程A和panic交替打印字符串,最初主协程与协程A、B全副退出。所以咱们能够看到,一个协程panic之后,是会导致所有的协程全副挂掉的,程序会整体退出,到这里咱们就验证了第一个问题的答案。
至于panic和协程A交替打印的起因,可能是因为panic也须要打印字符串。因为打印也是须要工夫的,当咱们执行panic这一行代码的时候,到panic真正触发所有协程挂掉,是须要肯定的工夫的(只管这个工夫很短暂),所以再这一小段时间内,咱们会看到交替打印的景象。
问题二
- 如果协程A产生了panic,其余协程是否能用recover捕捉到协程A的panic?
还是相似下面那段代码,咱们还能够再精简一下:
package mainimport ( "fmt" "time")func main() { defer func() { if e := recover(); e != nil { fmt.Println("recover_panic") } }() go func() { panic("goroutine2_panic") }() time.Sleep(2 * time.Second)}
咱们这次只开启一个协程,并在主协程中退出了recover,心愿它可能捕捉到子协程中的panic,然而后果未能如愿:
panic: goroutine2_panicgoroutine 6 [running]:main.main.func2() /Users/jiangbaiyan/go/src/awesomeProject/main.go:17 +0x39created by main.main /Users/jiangbaiyan/go/src/awesomeProject/main.go:16 +0x57Process finished with exit code 2
咱们看到,recover并没有失效。所以,哪个协程产生了panic,咱们就须要在哪个协程recover,咱们改成这样:
package mainimport ( "fmt" "time")func main() { go func() { defer func() { if e := recover(); e != nil { fmt.Println("recover_panic") } }() panic("goroutine2_panic") }() time.Sleep(2 * time.Second)}
后果胜利打印recover_panic字符串:
recover_panicProcess finished with exit code 0
所以咱们的答案也失去了验证:协程A产生panic,协程B无奈recover到协程A的panic,只有协程本人外部的recover能力捕捉本人抛出的panic。
最佳实际
咱们先假如有这样一个场景,咱们要开发一个客户端,这个客户端须要调用2个服务,这2个服务没有任何先后顺序的依赖,所以咱们能够开启2个goroutine,通过并发调用这两个服务来取得性能晋升。那么这个时候咱们方才所谈到的问题一就成了问题。
通常来讲,咱们不心愿其中一个服务调用失败,另一个服务调用也跟着失败,而是要继续执行完其余几个服务调用逻辑,这个时候咱们该怎么办呢?
聪慧的你肯定会想到,我在每个协程外部编写一个recover语句,让他接住每个协程本人可能会产生的panic,就可能解决一个协程panic而导致所有协程挂掉的问题了。咱们编写如下代码,这就是在业务开发中,联合问题二解决问题一的最佳实际:
// 并发调用服务,每个handler都会传入一个调用逻辑函数func GoroutineNotPanic(handlers ...func() error) (err error) { var wg sync.WaitGroup // 假如咱们要调用handlers这么多个服务 for _, f := range handlers { wg.Add(1) // 每个函数启动一个协程 go func(handler func() error) { defer func() { // 每个协程外部应用recover捕捉可能在调用逻辑中产生的panic if e := recover(); e != nil { // 某个服务调用协程报错,能够在这里打印一些谬误日志 } wg.Done() }() // 取第一个报错的handler调用逻辑,并最终向外返回 e := handler() if err == nil && e != nil { err = e } }(f) } wg.Wait() return}
以上办法调用示例:
// 调用示例func main() { // 调用逻辑1 aRpc := func() error { panic("rpc logic A panic") return nil } // 调用逻辑2 bRpc := func() error { fmt.Println("rpc logic B") return nil } err := GoroutineNotPanic(aRpc, bRpc) if err != nil { fmt.Println(err) }}
这样咱们就实现了一个通用的并发解决逻辑,每次调用咱们只须要把业务逻辑的函数传入即可,不必每次本人独自编写一套并发管制逻辑;同时调用逻辑2就不会因为调用逻辑1的panic而挂掉了,容错率更高。在业务开发中咱们能够参考这种实现形式~