【譯】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 標記完成。包替我們管理這些, 我們只需要等待程式完成並結束。