Go多协程并发环境下的错误处理是怎样的
Go多协程并发环境下的错误处理是怎样的
Go多协程并发环境下的错误处理是怎样的,针对这个问题,这篇文章详细介绍了相对应的分析和解答,希望可以帮助更多想解决这个问题的小伙伴找到更简单易行的方法。
引言
在Go语言中,我们通常会用到panic和recover来抛出错误和捕获错误,这一对操作在单协程环境下我们正常用就好了,并不会踩到什么坑。但是在多协程并发环境下,我们常常会碰到以下两个问题。假设我们现在有2个协程,我们叫它们协程A和B好了:
如果协程A发生了panic,协程B是否会因为协程A的panic而挂掉?
如果协程A发生了panic,协程B是否能用recover捕获到协程A的panic?
答案分别是:会、不能。
那么下面我们来一一验证,并给出在具体的业务场景下的最佳实践。
问题一
如果协程A发生了panic,协程B是否会因为协程A的panic而挂掉?
为了验证这个问题,我们写一段程序:
packagemainimport("fmt""time")funcmain(){//协程Agofunc(){for{fmt.Println("goroutine1_print")}}()//协程Bgofunc(){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_printgoroutinegoroutine1_print19goroutine1_printgoroutine1_printgoroutine1_printgoroutine1_print[runninggoroutine1_print]:goroutine1_printgoroutine1_printgoroutine1_printmain.main.func2()/Users/jiangbaiyan/go/src/awesomeProject/main.go:18+0x46createdbymain.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?
还是类似上面那段代码,我们还可以再精简一下:
packagemainimport("fmt""time")funcmain(){deferfunc(){ife:=recover();e!=nil{fmt.Println("recover_panic")}}()gofunc(){panic("goroutine2_panic")}()time.Sleep(2*time.Second)}
我们这次只开启一个协程,并在主协程中加入了recover,希望它能够捕获到子协程中的panic,但是结果未能如愿:
panic:goroutine2_panicgoroutine6[running]:main.main.func2()/Users/jiangbaiyan/go/src/awesomeProject/main.go:17+0x39createdbymain.main/Users/jiangbaiyan/go/src/awesomeProject/main.go:16+0x57Processfinishedwithexitcode2
我们看到,recover并没有生效。所以,哪个协程发生了panic,我们就需要在哪个协程recover,我们改成这样:
packagemainimport("fmt""time")funcmain(){gofunc(){deferfunc(){ife:=recover();e!=nil{fmt.Println("recover_panic")}}()panic("goroutine2_panic")}()time.Sleep(2*time.Second)}
结果成功打印recover_panic字符串:
recover_panicProcessfinishedwithexitcode0
所以我们的答案也得到了验证:协程A发生panic,协程B无法recover到协程A的panic,只有协程自己内部的recover才能捕获自己抛出的panic。
最佳实践
我们先假设有这样一个场景,我们要开发一个客户端,这个客户端需要调用2个服务,这2个服务没有任何先后顺序的依赖,所以我们可以开启2个goroutine,通过并发调用这两个服务来获得性能提升。那么这个时候我们刚才所谈到的问题一就成了问题。
通常来讲,我们不希望其中一个服务调用失败,另一个服务调用也跟着失败,而是要继续执行完其他几个服务调用逻辑,这个时候我们该怎么办呢?
聪明的你一定会想到,我在每个协程内部编写一个recover语句,让他接住每个协程自己可能会发生的panic,就能够解决一个协程panic而导致所有协程挂掉的问题了。我们编写如下代码,这就是在业务开发中,结合问题二解决问题一的最佳实践:
//并发调用服务,每个handler都会传入一个调用逻辑函数funcGoroutineNotPanic(handlers...func()error)(errerror){varwgsync.WaitGroup//假设我们要调用handlers这么多个服务for_,f:=rangehandlers{wg.Add(1)//每个函数启动一个协程gofunc(handlerfunc()error){deferfunc(){//每个协程内部使用recover捕获可能在调用逻辑中发生的panicife:=recover();e!=nil{//某个服务调用协程报错,可以在这里打印一些错误日志}wg.Done()}()//取第一个报错的handler调用逻辑,并最终向外返回e:=handler()iferr==nil&&e!=nil{err=e}}(f)}wg.Wait()return}
以上方法调用示例:
//调用示例funcmain(){//调用逻辑1aRpc:=func()error{panic("rpclogicApanic")returnnil}//调用逻辑2bRpc:=func()error{fmt.Println("rpclogicB")returnnil}err:=GoroutineNotPanic(aRpc,bRpc)iferr!=nil{fmt.Println(err)}}
这样我们就实现了一个通用的并发处理逻辑,每次调用我们只需要把业务逻辑的函数传入即可,不用每次自己单独编写一套并发控制逻辑;同时调用逻辑2就不会因为调用逻辑1的panic而挂掉了,容错率更高。
什么是go
go是golang的简称,golang 是Google开发的一种静态强类型、编译型、并发型,并具有垃圾回收功能的编程语言,其语法与 C语言相近,但并不包括如枚举、异常处理、继承、泛型、断言、虚函数等功能。
关于Go多协程并发环境下的错误处理是怎样的问题的解答就分享到这里了,希望以上内容可以对大家有一定的帮助,如果你还有很多疑惑没有解开,可以关注恰卡编程网行业资讯频道了解更多相关知识。
推荐阅读
-
go如何实现职责链模式
-
Go Callvis如何使用
-
go pprof如何使用
-
Go框架三件套Gorm、Kitex、Hertz怎么使用
-
Go语言单元测试和基准测试实例代码分析
-
go Realize怎么使用
goRealize怎么使用本文小编为大家详细介绍“goReal...
-
14天学会Go语言第四天:Array Map Slice 数组 和切片
-
如何从 PHP 过渡到 Golang?
我是PHP开发者,转Go两个月了吧,记录一下使用Golang怎么一步步开发新项目。本着有坑填坑,有错改错的宗...
-
零经验Go语言MVC框架,并推荐PHP函数库和MySQL表自动转struct
最近搭建k8s系统,为了调试自动部署用Go做了个MVC框架,这是个适用于喜欢Go语言练习的框架,零Go语言经验学习Go;配合Go模...
-
从php到Golang系统的演变