技巧分享:多 Goroutine 如何優(yōu)雅處理錯誤?
本文轉(zhuǎn)載自微信公眾號「腦子進(jìn)煎魚了」,作者陳煎魚。轉(zhuǎn)載本文請聯(lián)系腦子進(jìn)煎魚了公眾號。
大家好,我是煎魚。
在 Go 語言中,goroutine 的使用是非常頻繁的,因此在日常編碼的時候我們會遇到一個問題,那就是 goroutine 里面的錯誤處理,怎么做比較好?
這是來自我讀者群的問題。作為一個寵粉煎魚,我默默記下了這個技術(shù)話題。今天煎魚就大家來看看多 goroutine 的錯誤處理機(jī)制也有哪些!
一般來講,我們的業(yè)務(wù)代碼會是:
- func main() {
- var wg sync.WaitGroup
- wg.Add(2)
- go func() {
- log.Println("腦子進(jìn)煎魚了")
- wg.Done()
- }()
- go func() {
- log.Println("煎魚想報錯...")
- wg.Done()
- }()
- time.Sleep(time.Second)
- }
在上述代碼中,我們運(yùn)行了多個 goroutine。但我想拋出 error 的錯誤信息出來,似乎沒什么好辦法...
通過錯誤日志記錄
為此,業(yè)務(wù)代碼中常見的第一種方法:通過把錯誤記錄寫入日志文件中,再結(jié)合相關(guān)的 logtail 進(jìn)行采集和梳理。
但這又會引入新的問題,那就是調(diào)用錯誤日志的方法寫的到處都是。代碼結(jié)構(gòu)也比較亂,不直觀。
最重要的是無法針對 error 做特定的邏輯處理和流轉(zhuǎn)。
利用 channel 傳輸
這時候大家可能會想到 Go 的經(jīng)典哲學(xué):不要通過共享內(nèi)存來通信,而是通過通信來實(shí)現(xiàn)內(nèi)存共享(Do not communicate by sharing memory; instead, share memory by communicating)。
第二種的方法:利用 channel 來傳輸多個 goroutine 中的 errors:
- func main() {
- gerrors := make(chan error)
- wgDone := make(chan bool)
- var wg sync.WaitGroup
- wg.Add(2)
- go func() {
- wg.Done()
- }()
- go func() {
- err := returnError()
- if err != nil {
- gerrors <- err
- }
- wg.Done()
- }()
- go func() {
- wg.Wait()
- close(wgDone)
- }()
- select {
- case <-wgDone:
- break
- case err := <-gerrors:
- close(gerrors)
- fmt.Println(err)
- }
- time.Sleep(time.Second)
- }
- func returnError() error {
- return errors.New("煎魚報錯了...")
- }
輸出結(jié)果:
- 煎魚報錯了...
雖然使用 channel 后已經(jīng)方便了不少。但自己編寫 channel 總是需要關(guān)心一些非業(yè)務(wù)向的邏輯。
借助 sync/errgroup
因此第三種方法,就是使用官方提供的 sync/errgroup 標(biāo)準(zhǔn)庫:
- type Group
- func WithContext(ctx context.Context) (*Group, context.Context)
- func (g *Group) Go(f func() error)
- func (g *Group) Wait() error
- Go:啟動一個協(xié)程,在新的 goroutine 中調(diào)用給定的函數(shù)。
- Wait:等待協(xié)程結(jié)束,直到來自 Go 方法的所有函數(shù)調(diào)用都返回,然后返回其中的第一個非零錯誤(如果有的話)。
結(jié)合其特性能夠非常便捷的針對多 goroutine 進(jìn)行錯誤處理:
- func main() {
- g := new(errgroup.Group)
- var urls = []string{
- "http://www.golang.org/",
- "https://golang2.eddycjy.com/",
- "https://eddycjy.com/",
- }
- for _, url := range urls {
- url := url
- g.Go(func() error {
- resp, err := http.Get(url)
- if err == nil {
- resp.Body.Close()
- }
- return err
- })
- }
- if err := g.Wait(); err == nil {
- fmt.Println("Successfully fetched all URLs.")
- } else {
- fmt.Printf("Errors: %+v", err)
- }
- }
在上述代碼中,其表現(xiàn)的是爬蟲的案例。每一個計劃新起的 goroutine 都直接使用 Group.Go 方法。在等待和錯誤上,直接調(diào)用 Group.Wait 方法就可以了。
使用標(biāo)準(zhǔn)庫 sync/errgroup 這種方法的好處就是不需要關(guān)注非業(yè)務(wù)邏輯的控制代碼,比較省心省力。
進(jìn)階使用
在真實(shí)的工程代碼中,我們還可以基于 sync/errgroup 實(shí)現(xiàn)一個 http server 的啟動和關(guān)閉 ,以及 linux signal 信號的注冊和處理。以此保證能夠?qū)崿F(xiàn)一個 http server 退出,全部注銷退出。
參考代碼(@via 毛老師)如下:
- func main() {
- g, ctx := errgroup.WithContext(context.Background())
- svr := http.NewServer()
- // http server
- g.Go(func() error {
- fmt.Println("http")
- go func() {
- <-ctx.Done()
- fmt.Println("http ctx done")
- svr.Shutdown(context.TODO())
- }()
- return svr.Start()
- })
- // signal
- g.Go(func() error {
- exitSignals := []os.Signal{os.Interrupt, syscall.SIGTERM, syscall.SIGQUIT, syscall.SIGINT} // SIGTERM is POSIX specific
- sig := make(chan os.Signal, len(exitSignals))
- signal.Notify(sig, exitSignals...)
- for {
- fmt.Println("signal")
- select {
- case <-ctx.Done():
- fmt.Println("signal ctx done")
- return ctx.Err()
- case <-sig:
- // do something
- return nil
- }
- }
- })
- // inject error
- g.Go(func() error {
- fmt.Println("inject")
- time.Sleep(time.Second)
- fmt.Println("inject finish")
- return errors.New("inject error")
- })
- err := g.Wait() // first error return
- fmt.Println(err)
- }
內(nèi)部基礎(chǔ)框架有非常有這種代碼,有興趣的可以自己模仿著寫一遍,收貨會很多。
總結(jié)
在 Go 語言中 goroutine 是非常常用的一種方法,為此我們需要更了解 goroutine 配套的上下游(像是 context、error 處理等),應(yīng)該如何用什么來保證。
再在團(tuán)隊中形成一定的共識和規(guī)范,這么工程代碼閱讀起來就會比較的舒適,一些很坑的隱藏 BUG 也會少很多 :)