Go語言使用型別斷言來識別錯誤

2020-07-16 10:04:56
考慮一下 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 {
    //注意:不健壯
    return strings.Contains(err.Error(), "file does not exist")
}

但由於處理 I/O 錯誤的邏輯會隨著平台的變化而變化,因此這種方法很不健壯,同樣的錯誤可能會用完全不同的錯誤訊息來報告。檢查錯誤訊息是否包含特定的字串,這種方法在單元測試中還算夠用,但對於生產級的程式碼則遠遠不夠。

一個更可靠的方法是用專門的型別來表示結構化的錯誤值。OS 包定義了一個 PathError 型別來表示在與一個檔案路徑相關的操作上發生錯誤(比如 Open 或者 Delete),一個類似的 LinkError 用來表述在與兩個檔案路徑相關的操作上發生錯誤(比如 Symlink 和 Rename)。下面是 os.PathError 的定義:
package os

// PathError 記錄了錯誤以及錯誤相關的操作和檔案路徑
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("%#vn", err)
//輸出:
// &os.PathError{Op: "open", Path: "/no/such/file", Err:0x2}

這也是之前三個幫助函數的工作方式。比如,如下所示的 IsNotExist 判斷錯誤是否等於 syscall.ENOENT,或者等於另一個錯誤 os.ErrNotExist,或者是一個 *PathError,並且底層的錯誤是上面二者之一。
import (
    "errors"
    "syscall"
)
var ErrNotExist = errors.New("file does not exist")

// IsNotExist返回一個布林值,該值表明錯誤是否代表檔案或目錄不存在
// report that a file or directory does not exist. It is satisfied by
// ErrNotExist 和其他一些系統呼叫錯誤會返回 true
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"

當然,如果錯誤訊息已被 fmt.Errorf 這類的方法合併到一個大字串中,那麼 PathError 的結構資訊就丟失了。錯誤識別通常必須在失敗操作發生時馬上處理,而不是等到錯誤訊息返回給呼叫者之後。