7.11. 基於類型斷言區别錯誤類型

思考在os包中文件操作返迴的錯誤集合。I/O可以因爲任何數量的原因失敗,但是有三種經常的錯誤必須進行不同的處理:文件已經存在(對於創建操作),找不到文件(對於讀取操作),和權限拒絶。os包中提供了這三個幫助函數來對給定的錯誤值表示的失敗進行分類:

package os

func IsExist(err error) bool
func IsNotExist(err error) bool
func IsPermission(err error) bool

對這些判斷的一個缺乏經驗的實現可能會去檢査錯誤消息是否包含了特定的子字符串,

func IsNotExist(err error) bool {
    // NOTE: not robust!
    return strings.Contains(err.Error(), "file does not exist")
}

但是處理I/O錯誤的邏輯可能一個和另一個平台非常的不同,所以這種方案併不健壯併且對相同的失敗可能會報出各種不同的錯誤消息。在測試的過程中,通過檢査錯誤消息的子字符串來保證特定的函數以期望的方式失敗是非常有用的,但對於線上的代碼是不夠的。

一個更可靠的方式是使用一個專門的類型來描述結構化的錯誤。os包中定義了一個PathError類型來描述在文件路徑操作中涉及到的失敗,像Open或者Delete操作,併且定義了一個叫LinkError的變體來描述涉及到兩個文件路徑的操作,像Symlink和Rename。這下面是os.PathError:

package os

// PathError records an error and the operation and file path that caused it.
type PathError struct {
    Op   string
    Path string
    Err  error
}

func (e *PathError) Error() string {
    return e.Op + " " + e.Path + ": " + e.Err.Error()
}

大多數調用方都不知道PathError併且通過調用錯誤本身的Error方法來統一處理所有的錯誤。盡管PathError的Error方法簡單地把這些字段連接起來生成錯誤消息,PathError的結構保護了內部的錯誤組件。調用方需要使用類型斷言來檢測錯誤的具體類型以便將一種失敗和另一種區分開;具體的類型比字符串可以提供更多的細節。

_, err := os.Open("/no/such/file")
fmt.Println(err) // "open /no/such/file: No such file or directory"
fmt.Printf("%#v\n", err)
// Output:
// &os.PathError{Op:"open", Path:"/no/such/file", Err:0x2}

這就是三個幫助函數是怎麽工作的。例如下面展示的IsNotExist,它會報出是否一個錯誤和syscall.ENOENT(§7.8)或者和有名的錯誤os.ErrNotExist相等(可以在§5.4.2中找到io.EOF);或者是一個*PathError,它內部的錯誤是syscall.ENOENT和os.ErrNotExist其中之一。

import (
    "errors"
    "syscall"
)

var ErrNotExist = errors.New("file does not exist")

// IsNotExist returns a boolean indicating whether the error is known to
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist as well as some syscall errors.
func IsNotExist(err error) bool {
    if pe, ok := err.(*PathError); ok {
        err = pe.Err
    }
    return err == syscall.ENOENT || err == ErrNotExist
}

下面這里是它的實際使用:

_, err := os.Open("/no/such/file")
fmt.Println(os.IsNotExist(err)) // "true"

如果錯誤消息結合成一個更大的字符串,當然PathError的結構就不再爲人所知,例如通過一個對fmt.Errorf函數的調用。區别錯誤通常必須在失敗操作後,錯誤傳迴調用者前進行。