1. 程式人生 > 其它 >【譯】GO語言:管理多個錯誤

【譯】GO語言:管理多個錯誤

原文:https://medium.com/a-journey-with-go/go-multiple-errors-management-a67477628cf1

​ 關於開發者使用Go遇到的最大挑戰的年度調查報告中,錯誤管理是經常被爭論和反覆提起的話題。然而,當涉及到在併發環境中處理錯誤或為相同的 goroutine 組合多個錯誤時,Go 提供了很好的包,使多個錯誤的管理變得很容易

單個 goroutine,多個錯誤

例如,當您處理具有重試策略的程式碼時,將多個錯誤合併成一個非常有用。下面是一個基本的例子,其中我們需要收集生成的錯誤:

var data = []byte(
`a,b,c
foo
1,2,3
,",
`)

func main() {
  reader := csv.NewReader(bytes.NewBuffer(data))
  for {
    if _, err := reader.Read(); err != nil {
      if err == io.EOF {
        break
      }
      log.Printf(err.Error())
    }
  }
}

上面的程式讀取、解析一個 CSV 檔案,並且顯示報錯資訊。它可以更方便的對錯誤進行分組以獲得完整的報告。為了將錯誤合併成一個,我們需要在兩個很優秀的包中選擇一個:

  • 使用 HashiCorp 的 go-multierror ,可以將錯誤合併為一個標準錯誤

    func main() {
      var errs error
      reader := csv.NewReader(bytes.NewBuffer(data))
      for {
        if _, err := reader.Read(); err != nil {
          if err == io.EOF {
            break
          }
          errs = multierror.Append(errs, err)
        }
      }
      if errs != nil {
        log.Printf(errs.Error())
      }
    }
    

    接著錯誤報告可以被打印出來

  • 使用 Uber 的 multierr

    程式碼的實現是類似的,下面是輸出

    錯誤通過分號連線,沒有任何其他格式。

    關於每個包的效能,下面是一個具有較高失敗次數的程式的基準測試

    name                    time/op         alloc/op        allocs/op
    HashiCorpMultiErrors-4  6.01µs ± 1%     6.78kB ± 0%     77.0 ± 0%
    UberMultiErrors-4       9.26µs ± 1%     10.3kB ± 0%      126 ± 0%
    

    Uber 的速度稍慢,佔用記憶體更多。但是,這個包的設計目的是在收集到錯誤之後將其分組,而不是每次都新增錯誤。當對錯誤進行分組時,結果很接近,但是程式碼不夠優雅,因為它需要額外的步驟。以下是新的測試結果:

    name                    time/op         alloc/op        allocs/op
    HashiCorpMultiErrors-4  6.01µs ± 1%     6.78kB ± 0%     77.0 ± 0%
    UberMultiErrors-4       6.02µs ± 1%     7.06kB ± 0%     77.0 ± 0%
    
    

    這兩個包都利用了 Go 錯誤介面,它們都實現了自己的 Error() string 函式。

一個錯誤, 多個 goroutines

當多個 goroutine 來處理一個任務時,正確管理結果和聚合錯誤以確保程式的正確性是必要的。

讓我們從一個使用多個 goroutine 執行一系列操作的程式開始,每一個操作都持續一秒:

func main() {
  var wg sync.WaitGroup
  for i := 0; i < 4; i++ {
    wg.Add(1)
    go func() {
      defer wg.Done()
      if err := action(); err != nil {
        return
      }
      if err := action(); err != nil {
        return
      }
      if err := action(); err != nil {
        return
      }
    }()
  }
  wg.Wait()
}

為了解釋錯誤的傳播,第三個 goroutine 的第一個操作會失敗,下面時正在發生的事情:

正如預期的那樣,程式大約需要 3 秒鐘,因為大多數 goroutine 需要經歷三個操作,每個操作花費一秒:

go run .  0.30s user 0.19s system 14% cpu 3.274 total

然而,我們可能希望讓這些 goroutine 互相依賴,並在其中一個失敗時取消它們以避免不必要的工作,解決方案是新增一個上下文,一旦 goroutine 失敗,它就會取消它。

這個功能這是 errgroup 包所提供的;當一組 goroutine 工作時的錯誤和上下文傳播。

下面時使用 errgroup包的程式碼:

func main() {
  g, ctx := errgroup.WithContext(context.Background())
  for i := 0; i < 4; i++ {
    g.Go(func () error {
      if err := action(ctx); err != nil {
        return err
      }
      if err := action(ctx); err != nil {
        return err
      }
      if err := action(ctx); err != nil {
        return err
      }
      return nil
    })
  }
  if err := g.Wait(); err != nil {
    log.Printf(err.Error())
  }
}

現在程式執行的更快了,因為它在發生錯誤時傳播了取消上下文:

go run .  0.30s user 0.19s system 38% cpu 1.269 total

該包提供的另一個好處是,我們不再需要擔心 goroutine 的新增和對 goroutine 標記完成。包替我們管理這些, 我們只需要等待程式完成並結束。