当前位置: 首页 > 科技观察

技巧分享:多Goroutines如何优雅处理错误?

时间:2023-03-17 13:29:11 科技观察

本文转载自微信公众号《我的脑子是炸鱼》,作者陈建宇。转载本文请联系脑筋急转弯公众号。大家好,我是炸鱼。在Go语言中,goroutine的使用频率很高,所以我们在日常编码中会遇到一个问题,就是goroutine中的错误处理,如何做的比较好?这是我的读者提出的问题。作为一个炸鱼爱好者,我默默的把这个技术题目记下来了。今天我们就来看看multi-goroutine的错误处理机制吧!一般来说,我们的业务代码会是:funcmain(){varwgsync.WaitGroupwg.Add(2)gofunc(){log.Println("brainIntofriedfish")wg.Done()}()gofunc(){log.println("炸鱼要报错...")wg.Done()}()time.Sleep(time.Second)}在上面的代码中,我们运行了多个goroutines。但是想把报错的报错信息抛出,貌似没有什么好办法。。。通过报错日志来实现,业务代码中常见的第一种方法:通过将报错记录写入日志文件,结合与相关的logtail收集整理。但是这样会引入一个新的问题,就是到处都写着调用错误日志的方法。代码结构也比较凌乱,不直观。最重要的是无法针对错误做具体的逻辑处理和流程。这时候你可能会想到Go在使用通道传输时的经典哲学:不要通过共享内存进行通信;相反,通过通信共享内存。第二种方法:在多个goroutine中使用channel传递错误:)gofunc(){err:=returnError()iferr!=nil{gerrors<-err}wg.Done()}()gofunc(){wg.Wait()close(wgDone)}()select{case<-wgDone:breakcaseerr:=<-gerrors:close(gerrors)fmt.Println(err)}time.Sleep(time.Second)}funcreturnError()error{returnerrors.New("炸鱼报错...")}输出结果:煎鱼报错。。。虽然用通道后方便多了。但是自己写channel总是需要关心一些非业务导向的逻辑。借助sync/errgroup,第三种方法是使用sync/errgroup官方标准库:typeGroupfuncWithContext(ctxcontext.Context)(*Group,context.Context)func(g*Group)Go(ffunc()error)func(g*Group)Wait()errorGo:启动一个协程,在一个新的协程中调用给定的函数。等待:等待协程结束,直到Go方法的所有函数调用都返回,然后返回第一个非零错误(如果有)。结合其特点,对于多个goroutine的错误处理非常方便:funcmain(){g:=new(errgroup.Group)varurls=[]string{"http://www.golang.org/","https://golang2.eddycjy.com/","https://eddycjy.com/",}for_,url:=rangeurls{url:=urlg.Go(func()error{resp,err:=http.Get(url)iferr==nil{resp.Body.Close()}returnerr})}iferr:=g.Wait();err==nil{fmt.Println("成功获取所有URL。")}else{fmt.printf("Errors:%+v",err)}}上面的代码中,展示了爬虫的情况。每个新调度的goroutine都直接使用Group.Go方法。对于等待和错误,直接调用Group.Wait方法即可。使用标准库sync/errgroup方法的好处是不需要关注非业务逻辑的控制代码,省心省力。高级使用在实际工程代码中,我们还可以实现基于sync/errgroup的http服务器的启动和关闭,以及linuxsignal信号的注册和处理。这确保了一个http服务器退出并且所有注销退出。参考代码(@via毛老师)如下:funcmain(){g,ctx:=errgroup.WithContext(context.Background())svr:=http.NewServer()//httpserverg.Go(func()error{fmt.Println("http")gofunc(){<-ctx.Done()fmt.Println("httpctxdone")svr.Shutdown(context.TODO())}()returnsvr.Start()})//signalg.Go(func()error{exitSignals:=[]os.Signal{os.Interrupt,syscall.SIGTERM,syscall.SIGQUIT,syscall.SIGINT}//SIGTERMisPOSIXspecificsig:??=make(chanos.Signal,len(exitSignals))signal.Notify(sig,exitSignals...)for{fmt.Println("signal")select{case<-ctx.Done():fmt.Println("signalctxdone")returnctx.Err()case<-sig://dosomethingreturnil}}})//injecterrog.Go(func()error{fmt.Println("inject")time.Sleep(time.Second)fmt.Println("injectfinish")returnerrors.New("injecterror")})err:=g.Wait()//firstrorreturnfmt.Println(err)}内部基础框架有很多这样的代码,有兴趣的可以自己copy写一下,收货多多.综上所述,goroutine是Go语言中非常常用的一个方法。为此,我们需要更多地了解goroutine的上下游(如上下文、错误处理等),以及如何使用和保障。然后在团队中形成一定的共识和规范,这样工程代码读起来会更舒服,一些隐藏的bug也会少很多:)