「Go工具箱」一個簡單、易用的多錯誤管理包:go-multierror

yudotyang發表於2022-11-07

公眾號 「Go學堂」

大家好,我是漁夫子。本號新推出「Go 工具箱」系列,意在給大家分享使用 go 語言編寫的、實用的、好玩的工具。

今天給大家推薦的是一個多錯誤管理包工具:go-multierror。

該包可以將多個錯誤合併成一個標準的 error,使得多個錯誤管理變得更容易。同時,該包和 go 標準庫中的 error 包完全相容,包括 As、Is 和 Unwrap 函式。

小檔案

go-multierror 小檔案
star 1.7k used by 38.6k
contributors 16 作者 HashiCorp(機構)
功能簡介 多錯誤管理包。可以將多個錯誤合併成一個標準的 error,使得多個錯誤管理變得更容易。
專案地址 https://github.com/hashicorp/go-multierror^[1]^
相關知識 error 處理

一、安裝

安裝

使用 go get 進行安裝

go get github.com/hashicorp/go-multierror

go 版本要求

該最新包需要依賴於 Go 的 1.13 或更高版本,因為 error 中的 wrap 功能是從 1.13 版本開始的。如果你當前的 go 版本低於 1.13,那麼可以使用該包的 v1.0.0 tag,該版本不依賴於 1.13 中的 wrap 功能。

go get github.com/hashicorp/go-multierror@v1.0.0

知識點:在 Go 1.13 版本之前,標準庫對 error 的支援僅有 errors.New()和 fmt.Errorf()兩個函式來構造 error 例項。從 1.13 版本開始,在 errors 和 fmt 標準庫包中引入了新功能以簡化處理包含其他錯誤的錯誤,稱之為鏈式 error。其中就包含 errors.Unwrap()、errors.Is()和 errors.As(),以及 fmt.Errorf 中引入了%w 動詞以建立 wrapError。

二、基本使用

mutlierror 包的使用也非常簡單。下面我們看下其主要的使用。

構建錯誤列表

透過 mutierror 包中的 Append 函式可以建立錯誤列表。該函式的行為非常類似 go 內建的 append 函式。Append 的第一個引數無論是 nil、multierror.Error 或者其他型別的 error,該函式都會返回一個 multierror.Error 型別的值,並將 Append 中第二個引數中的 err 加入到 multierror.Error 的列表中。

var result error

if err := step1(); err != nil {
 result = multierror.Append(result, err)
}
if err := step2(); err != nil {
 result = multierror.Append(result, err)
}

return result

自定義格式化輸出

透過指定 multierror.Error 的例項變數中的 ErrorFormat 屬性,就可以自定義 Error() string 的輸出格式:

var result *multierror.Error

// ... accumulate errors here, maybe using Append

if result != nil {
 result.ErrorFormat = func([]error) string {
  return "errors!"
 }
}

訪問錯誤列表

multierror.Error 實現了 error 介面,所以即使呼叫者不知道返回的錯誤型別是否是 multierror,該錯誤依然能正常工作。同時,我們也可以透過型別斷言的方式來校驗返回的錯誤是否是 multierror.Error 型別,以便可以訪問 multierror.Error 中的所有 error。

if err := something(); err != nil {
 if merr, ok := err.(*multierror.Error); ok {
  // Use merr.Errors
 }
}

當然,也可以使用 go 內建包 errors.Unwrap()函式對 mutlierror.Errors 依次解析,直到所有的 error 都被解析完成。

知識點:內建包中的 errors.Unwrap()函式,可以對 error 層層拆解。對於自定義的 error 型別,不僅要實現 Error()函式,同時也需要實現 Unwrap 函式。

errors.Unwrap()函式的原始碼如下:

func Unwrap(err error) error {
 u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

如果引數 err 沒有實現 Unwrap()函式,則說明是基礎 error,直接返回 nil,否則呼叫元 err 實現的 Unwrap()函式並返回。

提取特定的 error 值

標準庫中的 errors.As 函式可以直接從 multierror.Error 中提取一個特定的 error:

// Assume err is a multierror value
err := somefunc()

// We want to know if "err" has a "RichErrorType" in it and extract it.
var errRich RichErrorType
if errors.As(err, &errRich) {
 // It has it, and now errRich is populated.
}

知識點:標準庫中的 errors.As 函式會呼叫 Unwrap 函式,將 err 層層拆解,如果拆解到的 error 和目標 error 型別相同,則將該 error 賦值給目標引數 errRich。

檢查 multierror.Error 中是否有具體的錯誤值

有時候一些函式會返回具體的錯誤值,比如 os 包中返回的 ErrNotExists。所以,我們就可以透過 errors.Is 函式來檢查 multierror.Error 中是否存在具體的錯誤值。

// Assume err is a multierror value
err := somefunc()
if errors.Is(err, os.ErrNotExist) {
 // err contains os.ErrNotExist
}

知識點:errors.Is 用來判斷鏈式 err 中是否有具體的 error 值(通常稱之為哨兵 error)

錯誤處理

在實際使用中,錯誤都是由從函式中建立並返回的。呼叫者用 if 語句判斷返回的錯誤是否為 nil(error 飛初始化的值)來判斷錯誤是否存在。那麼,在 multierror.Error 型別中,何時返回 nil,何時返回錯誤呢? 在 multierror.Error 的例項中,可以透過該型別的 ErrorOrNil 方法來返回錯誤或 nil。該函式內部實現中判斷該例項中的 Errors 切片是否為空,如果不為空,則返回該例項,否則返回 nil。

var result *multierror.Error

// ... accumulate errors here

// Return the `error` only if errors were added to the multierror, otherwise
// return nil since there are no errors.
return result.ErrorOrNil()

ErrorOrNil 函式的實現如下:

func (e *Error) ErrorOrNil() error {
 if e == nil {
  return nil
 }
 if len(e.Errors) == 0 {
  return nil
 }

 return e
}

知識點:在 Go 中錯誤一般是從函式中建立並作為值返回的。呼叫者需要使用 if 語句判斷返回的錯誤是否為 nil 來判斷錯誤是否存在。 同時,在 Go 中,函式有多個返回值時,錯誤一般放到最後。

三、實現原理分析

multierror.Error 型別的定義

multierror.Error 型別的結構很簡單,因為要實現多錯誤管理,所以有一個 error 型別的切片;另外還有一個 ErrorformatFunc 函式型別,用於格式化輸出 Error 的描述。如下:

type Error struct {
 Errors      []error
 ErrorFormat ErrorFormatFunc
}

type ErrorFormatFunc func([]error) string

這裡我們需要提及到 golang 中的 error 型別的知識點。

知識點:Golang 中的 error 實質上就是一個簡單的介面型別。只要實現了這個介面,就可以將其視為一種 error。

type error interface {
    Error() string
}

所以,multierror.Error 型別也實現了 Error()方法:

func (e *Error) Error() string {
 fn := e.ErrorFormat
 if fn == nil {
  fn = ListFormatFunc
 }

 return fn(e.Errors)
}

multierror.Error 型別實現了 error 介面,那麼該型別的變數就可以儲存到 error 介面的變數中。

知識點:任何型別只要實現了 interface 型別的所有方法,就可以聲稱該型別實現了這個介面,該型別的變數就可以儲存到 interface 變數中。

multierror.Append 函式的實現

在基本使用一節我們提到,可以透過 Append 函式來構建一個具體的多錯誤值例項 multierror.Error。這個本質上是將 error 值鍵入到 multierror.Error 型別的 Errors 切片中。

同時,我們提到,該 Append 函式無論是我們看下是如何實現:

  • 首先透過型別斷言 err.(type)來判斷 err 的型別

  • 如果引數中的 err 不是 multierror.Error 型別,則新構建一個 errors 切片,將 err 和 errs 都加入到切片中,然後再構建一個空 multierrors.Error 型別的例項,然後再遞迴呼叫 multierrors.Append 函式。

  • 如果引數中的 err 的型別就是 Error,那麼就將 errs 錯誤加入到 Error 結構型別中的 Errors 切片中。

具體實現如下:

func Append(err error, errs ...error) *Error {
 switch err := err.(type) {
 case *Error:
  // Typed nils can reach here, so initialize if we are nil
  if err == nil {
   err = new(Error)
  }

  // Go through each error and flatten
  for _, e := range errs {
   switch e := e.(type) {
   case *Error:
    if e != nil {
     err.Errors = append(err.Errors, e.Errors...)
    }
   default:
    if e != nil {
     err.Errors = append(err.Errors, e)
    }
   }
  }

  return err
 default:
  newErrs := make([]error, 0, len(errs)+1)
  if err != nil {
   newErrs = append(newErrs, err)
  }
  newErrs = append(newErrs, errs...)

  return Append(&Error{}, newErrs...)
 }
}

自定義格式化錯誤輸出實現

在基本使用中提到,可以給 multierrors.Error 型別中的 ErrorFormat 屬性賦值一個輸出錯誤的函式,這樣就能按自定義函式的格式將錯誤列表輸出了。該輸出的實現實際是在 Error 函式中定義的:

func (e *Error) Error() string {
 fn := e.ErrorFormat
 if fn == nil {
  fn = ListFormatFunc
 }

 return fn(e.Errors)
}

var result *multierror.Error
if result != nil {
 result.ErrorFormat = func([]error) string {
  return "errors!"
 }
}

result.Error() //就會按照ErrorFormat函式輸出錯誤

應用場景

多錯誤管理的應用場景一般是用在一個函式的邏輯中需要把所有的錯誤都返回的情況。比如在服務啟動時,對 redis、kafka、mysql 等各種資源初始化場景,可以把所有相關資源初始化的錯誤都返回。還有一種場景就是在 web 請求中,校驗請求引數時,返回所有引數的校驗錯誤給客戶端的場景。

—特別推薦—

特別推薦:一個專注go專案實戰、專案中踩坑經驗及避坑指南、各種好玩的go工具的公眾號。「Go學堂」,專注實用性,非常值得大家關注。點選下方公眾號卡片,直接關注。關注送《100個go常見的錯誤》pdf文件。

參考資料

[1] github.com/hashicorp/go-multierror

本作品採用《CC 協議》,轉載必須註明作者和本文連結
大家好,我是Go學堂的漁夫子,歡迎大家關注Go學堂,一起系統化的分享、學習Go相關的知識。

相關文章