天天看點

Go 1.13 之後的 error 檢查Go 1.13 之後的 error 檢查起步error 從何而來如何檢查 error1.13+ 如何檢查 error占位符 %w感謝

Go 1.13 之後的 error 檢查

文章目錄

  • Go 1.13 之後的 error 檢查
  • 起步
  • error 從何而來
  • 如何檢查 error
  • 1.13+ 如何檢查 error
    • Unwrap
    • errors.Is
    • errors.As
  • 占位符 %w
  • 感謝

起步

如果說 Go 有很多诟病的地方,那麼 Go 中 error 的處理一定可以擠進吐槽榜單前十。既然

try

語句提議被一拒再拒,我們也隻好用着古老的 if 篩選錯誤。Go 官方并非沒有意識到 error 的雞肋問題,于是在 Go 1.13 提出了新解決方案,總的說來就是“三個 api + 一個格式占位符”。

error 從何而來

在 Go 中,error 從何而來呢?熟練使用 Go 的人一定知道以下幾種方式:

  • errors.New
  • fmt.Errorf
  • 直接傳回函數調用後得到的 error
  • 定義一個結構體,實作 error 接口(

    type error Interface { Error() string }

以“打開一個檔案”為例,按照上面的處理方式代碼可以分别是:

// errors.New
func openConfigFile(path string) error {
	_, err := os.Open(path)
	if err != nil {
	    // 傳回新的 error 執行個體
		return errors.New("can not open the specific file")
	}
	return nil
}
           
// fmt.Errorf
func openConfigFile(path string) error {
	_, err := os.Open(path)
	if err != nil {
	    // 傳回新的 error 執行個體,同時包含了實際 error
		return fmt.Errorf("can not open the specific file, reason: %v", err)
	}
	return nil
}
           
// return error that called function returned
func openConfigFile(path string) error {
	_, err := os.Open(path)
	if err != nil {
	    // 直接傳回得到的 error,不做任何處理
		return err
	}
	return nil
}
           
// 自定義 error
type OpenErr struct {
	Err error  // 存放原始 error
}

// 實作 error 接口
func (*OpenErr) Error() string {
	return "can not open the specific file"
}

func openConfigFile(path string) error {
	_, err := os.Open(path)
	if err != nil {
		return &OpenErr{Err:err}
	}
	return nil
}
           

上述四種方法各有千秋。如方法一,errors.New 會傳回一個新的 error,其存放的資料就是我們傳入的 text(“can not open the specific file”)。采用這種方式通常是為了告訴調用者出錯了,但實際的錯誤細節不願暴露。對調用者來說,他可能不太關心出了什麼錯,隻在乎有沒有出錯。

方法二跟方法一相同,也會隐藏原始錯誤(這裡指錯誤類型),但通常會将原始錯誤的字元串說明一起傳回。在該處理方式中,“can not open the specific file” 為額外提示語,“reason: %v” 顯示錯誤細節。一般來講,調用 fmt.Errorf 更大幾率是要産生一個給人看的錯誤,而不是讓代碼去解析(盡管并非不能)。

方法三,通常是函數調用者需要根據 error 的實際類型,或實際值,确定下一步的執行政策。也就是說它需要解析 error,是以函數傳回“原味” error。這樣做的缺點是,沒辦法對 error 添加額外資訊。

方法四可以認為是上述三種方法的集合,既可以保留原始 error,還可以添加額外資訊。通過這些中繼資料随意組合,調用者想要的樣子它都有。缺點就是代碼要多寫一些。

如何檢查 error

現在 error 有了,我們應該如何檢查錯誤呢?在 1.13 之前,常見有:1. 比較值;2. 比較類型。

拿官方源碼舉例更具說服力。我們先來看看比較值。

func (db *DB) QueryContext(ctx context.Context, 
                           query string, 
                           args ...interface{}) (*Rows, error) {
	var rows *Rows
	var err error
	for i := 0; i < maxBadConnRetries; i++ {
		rows, err = db.query(ctx, query, args, cachedOrNewConn)
		if err != driver.ErrBadConn {  // 比較值
			break
		}
	}
	if err == driver.ErrBadConn {  // 比較值
		return db.query(ctx, query, args, alwaysNewConn)
	}
	return rows, err
}

func (db *DB) Query(query string, 
                    args ...interface{}) (*Rows, error) {
	return db.QueryContext(context.Background(), query, args...)
}
           

上述是

sql.Query

的源碼,确定處理 ErrBadConn 錯誤,就是通過比較值實作的。

比較類型處理 error 在 Go 源碼中更常用,主要是這種處理方式更靈活,錯誤資訊更豐富。

我們先寫一個 demo,代碼的主要目的是通路某網站。通路過程中可能出現各種異常,而我們隻處理逾時異常。

// http 用戶端
client := &http.Client{
	Timeout:       3 * time.Second,  // 3 秒逾時
}
// 請求不存在的 url
_, err := client.Get("http://www.meiyuouzhegewangzhan.com")
if err != nil {
	if os.IsTimeout(err) { // 逾時 err
		fmt.Println("timeout")
	} else {  // 其他 err
		fmt.Println("other errors")
	}
}
           

從上得知,判斷一個 error 是否是逾時異常,需要調用

os.IsTimeout

,源碼如下。

func IsTimeout(err error) bool {
    // 類型斷言
	terr, ok := underlyingError(err).(timeout)
	return ok && terr.Timeout()
}

func underlyingError(err error) error {
   // 傳回實際 err
	switch err := err.(type) {
	case *PathError:
		return err.Err
	case *LinkError:
		return err.Err
	case *SyscallError:
		return err.Err
	}
	// 如果沒有潛在 err, 傳回自身
	return err
}
           

不論是

IsTimeout

還是

underlyingError

都借助類型比較實作對不同錯誤類型做處理。另外,繼續跟進 PathError、LinkError、SysCallError 你會發現,這類 error 都是之前提到的第四種方法。它們包裝了原始 error,但又會在一定場合下把它取出來(如上述代碼中的

err.Err

)。

1.13+ 如何檢查 error

不得不承認,過去檢查 error 的方式有點麻煩,尤其是在 error 被包裝過多時。假設一個 error 被包裝了 3 層,此時又需要檢查最裡層的 error,意味着代碼需要這樣寫:

e1, _ := e.(Err1)
e2, _ := e1.(Err2)
e3, _ := e2.(Err3)
if e3 == targetErr {
    // handle
}
           

為了避免這種麻煩,1.13 開始,Go 提供了兩個方法,用于檢查鍊式中的 error。分别是比較值的

errors.Is

,以及比較類型的

errors.As

。想支援 error 鍊的檢查,要求結構體(或其他錯誤類型)實作匿名接口:

interface { Unwrap() error }

Unwrap

Unwrap 接口很好了解,用于傳回結構體中的原始 error。拿 PathError 的源碼舉例:

type PathError struct {
	Op   string
	Path string
	Err  error
}

func (e *PathError) Unwrap() error { return e.Err }
           

errors.Is

errors.Is 通過值比較确定錯誤,這裡有易錯點。由于許多 error 是指針類型,而指針類型的比較是:比較指針變量存放的位址是否相同。用代碼解釋更容易了解:

err1 := errors.New("error")
err2 := errors.New("error")

// 指針比較
fmt.Println(err1 == err2) // false

// 值比較
err1Elem := reflect.ValueOf(err1).Elem().Interface()
err2Elem := reflect.ValueOf(err2).Elem().Interface()
fmt.Println(err1Elem == err2Elem) // true
           

err1 與 err2 本質上可以認為是一個 error,連錯誤資訊都相同,但直接比較會得到 false。這是因為 err1 與 err2 有不同的位址。第二種方式則是将兩個指針指向的結構體取出來,比較兩個結構體值,進而獲得 true 的結果。

官方包對這類問題的處理常常是,定義一組錯誤變量,在之後的整個程式運作期間都使用已經指派的錯誤變量——確定同一類錯誤的指針總是指向同一個位址。

// go 源碼
var (
	// ErrInvalid indicates an invalid argument.
	// Methods on File will return this error when the receiver is nil.
	ErrInvalid = errInvalid() // "invalid argument"

	ErrPermission = errPermission() // "permission denied"
	ErrExist      = errExist()      // "file already exists"
	ErrNotExist   = errNotExist()   // "file does not exist"
	ErrClosed     = errClosed()     // "file already closed"
	ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
)

func errInvalid() error    { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error      { return oserror.ErrExist }
func errNotExist() error   { return oserror.ErrNotExist }
func errClosed() error     { return oserror.ErrClosed }
func errNoDeadline() error { return poll.ErrNoDeadline }
           

判斷檔案是否存在的兩種方式:

var err error
f, err := os.Open("不存在檔案")
defer f.Close()

// 方法1
if os.IsNotExist(err) { // 進入 if stmt
	fmt.Println("檔案不存在")
}
// 方法2
if errors.Is(err, os.ErrNotExist) { // 進入 if stmt
	fmt.Println("檔案不存在")
}
           

是以根據以上特性,errors.Is 幾乎不能處理 error 鍊。聰明的官方庫怎麼會想不到這點,解決方案要從源碼找:

// go 源碼
func Is(err, target error) bool {
	...
	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
	    // 值比較
		if isComparable && err == target {
			return true
		}
		// 調用 Is 比較
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		...
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}
           

errors.Is 會先進行值比較,如果失敗,那就有 Is 調用 Is,是以我們可以給自定義的 error 實作 Is 方法,用于填寫 error 是否相等的處理邏輯。

// Err1
type Err1 struct {
	Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
func (e * Err1) Is(other error) bool {
	v1 := reflect.ValueOf(e)
	v2 := reflect.ValueOf(other)
	// 如果是空指針直接傳回
	if v1.IsNil() || v2.IsNil() {
		return false
	}
	// 取出指針指向的變量
	v1 = v1.Elem()
	if v2.Kind() == reflect.Ptr {
		v2 = v2.Elem()
	}
	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// Err2
type Err2 struct {
	Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
func (e *Err2) Is(other error) bool {
	v1 := reflect.ValueOf(e)
	v2 := reflect.ValueOf(other)
	// 如果是空指針直接傳回
	if v1.IsNil() || v2.IsNil() {
		return false
	}
	// 取出指針指向的變量
	v1 = v1.Elem()
	if v2.Kind() == reflect.Ptr {
		v2 = v2.Elem()
	}
	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// Err3
type Err3 struct {
	Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
func (e *Err3) Is(other error) bool {
	v1 := reflect.ValueOf(e)
	v2 := reflect.ValueOf(other)
	// 如果是空指針直接傳回
	if v1.IsNil() || v2.IsNil() {
		return false
	}
	// 取出指針指向的變量
	v1 = v1.Elem()
	if v2.Kind() == reflect.Ptr {
		v2 = v2.Elem()
	}
	return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}

// 産生 error
func genErr() error {
	return &Err1{
		Err: &Err2{
			Err: &Err3{
				Err: nil,
			},
		},
	}
}

func main() {
	err := genErr()
	err3 := &Err3{Err:nil}
	fmt.Println(errors.Is(err, err3))
}
           

代碼明顯累贅起來,往下看,還有更優雅的辦法。

errors.As

當存在錯誤鍊時,我們更傾向于用類型定位錯誤,使用 errors.As 而不是 errors.Is。這樣做也能避免備援的 Is 方法。

// Err1
type Err1 struct {
	Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }

// Err2
type Err2 struct {
	Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }

// Err3
type Err3 struct {
	Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }

// 産生 error
func genErr() error {
	return &Err1{
		Err: &Err2{
			Err: &Err3{
				Err: nil,
			},
		},
	}
}

func main() {
	err := genErr()
	var err3 *Err3
	fmt.Println(errors.As(err, &err3)) // 第二個參數要求是 指針的位址
}
           

占位符 %w

大多數情況下我們用不着大動幹戈的自定義錯誤類型,而喜歡采用第二種方式

fmt.Errorf

。從 1.13 開始,使用 %w 占位符會把原始 error 包裹起來放到 err 字段裡,并建構一個新的 error 字元串放到 msg 中,對應的結構體就是 wrapError:

type wrapError struct {
	msg string
	err error
}

func (e *wrapError) Error() string {
	return e.msg
}

func (e *wrapError) Unwrap() error {
	return e.err
}
           

也就是說,以下用法不會丢失原始 error,反而會建構 error 鍊,友善 errors.Is 與 errors.As 進行錯誤檢查。

if _, err := os.Open("xxx.txt") {
    return fmt.Errorf("failed open, reason: %w", err)
}
           

感謝

  • 參考 Working with Errors in Go 1.13