Go 1.13 之後的 error 檢查

Go 1.13 之後的 error 檢查

起步

如果說 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...)
}
var ErrBadConn = errors.New("driver: bad connection")

上述是 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)
}

感謝

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章