多 Goroutine 如何優雅處理錯誤?

大家好,我是煎魚。

在 Go 語言中,goroutine 的使用是非常頻繁的,因此在日常編碼的時候我們會遇到一個問題,那就是 goroutine 裏面的錯誤處理,怎麼做比較好?

這是來自我讀者羣的問題。作爲一個寵粉煎魚,我默默記下了這個技術話題。今天煎魚就大家來看看多 goroutine 的錯誤處理機制也有哪些!

一般來講,我們的業務代碼會是:

func main() {
 var wg sync.WaitGroup
 wg.Add(2)
 go func() {
  log.Println("腦子進煎魚了")
  wg.Done()
 }()
 go func() {
  log.Println("煎魚想報錯...")
  wg.Done()
 }()

 time.Sleep(time.Second)
}

在上述代碼中,我們運行了多個 goroutine。但我想拋出 error 的錯誤信息出來,似乎沒什麼好辦法...

通過錯誤日誌記錄

爲此,業務代碼中常見的第一種方法:通過把錯誤記錄寫入日誌文件中,再結合相關的 logtail 進行採集和梳理。

但這又會引入新的問題,那就是調用錯誤日誌的方法寫的到處都是。代碼結構也比較亂,不直觀。

最重要的是無法針對 error 做特定的邏輯處理和流轉。

利用 channel 傳輸

這時候大家可能會想到 Go 的經典哲學:不要通過共享內存來通信,而是通過通信來實現內存共享(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("煎魚報錯了...")
}

輸出結果:

煎魚報錯了...

雖然使用 channel 後已經方便了不少。但自己編寫 channel 總是需要關心一些非業務向的邏輯。

藉助 sync/errgroup

因此第三種方法,就是使用官方提供的 sync/errgroup 標準庫:

type Group
    func WithContext(ctx context.Context) (*Group, context.Context)
    func (g *Group) Go(f func() error)
    func (g *Group) Wait() error

結合其特性能夠非常便捷的針對多 goroutine 進行錯誤處理:

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)
 }
}

在上述代碼中,其表現的是爬蟲的案例。每一個計劃新起的 goroutine 都直接使用 Group.Go 方法。在等待和錯誤上,直接調用 Group.Wait 方法就可以了。

使用標準庫 sync/errgroup 這種方法的好處就是不需要關注非業務邏輯的控制代碼,比較省心省力。

進階使用

在真實的工程代碼中,我們還可以基於 sync/errgroup 實現一個 http server 的啓動和關閉 ,以及 linux signal 信號的註冊和處理。以此保證能夠實現一個 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)
}

內部基礎框架有非常有這種代碼,有興趣的可以自己模仿着寫一遍,收貨會很多。

總結

在 Go 語言中 goroutine 是非常常用的一種方法,爲此我們需要更瞭解 goroutine 配套的上下游(像是 context、error 處理等),應該如何用什麼來保證。

再在團隊中形成一定的共識和規範,這麼工程代碼閱讀起來就會比較的舒適,一些很坑的隱藏 BUG 也會少很多 :)

關注煎魚,吸取他的知識 👆

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/NX6kVJP-RdUzcCmG2MF31w