Golang 學習——error 錯誤處理淺談

相守之路發表於2020-05-10

Golang中error錯誤處理淺談

在解析了Golang中error和建立error的原始碼後(Golang學習——error和建立error原始碼解析)。

對error有了一定理解,不過error處理才是實際開發中非常重要的一點。

Golang中的error處理是一門大學問,寫出優雅又正確的處理程式碼是比較考驗編碼功底和知識廣度,深度的。

今天就先淺談一下Golang中的錯誤處理。

1.== 比較

直接進行比較也是一種方式,但是有種硬編碼的感覺,必須事先確定好錯誤型別或已經知道要發生的錯誤是什麼型別的,這樣在錯誤比較的時候才能處理得當。

讓我們通過一個例子來理解這個問題。

filepath包的Glob函式用於返回與模式匹配的所有檔案的名稱。當模式出現錯誤時,該函式將返回一個錯誤ErrBadPattern

filepath包中定義了ErrBadPattern,如下所述:

var ErrBadPattern = errors.New("syntax error in pattern")

errors.New()用於建立新的錯誤。模式出現錯誤時,由Glob函式返回ErrBadPattern

實戰看一下就明白:

files, error := filepath.Glob("[")
if error != nil && error == filepath.ErrBadPattern {
    fmt.Println("error:", error)
    return
}
fmt.Println("matched files:", files)

輸出:

error: syntax error in pattern

我們想返回一個匹配 “[” 模式的檔案,如果發生錯誤會與我們預料中的錯誤型別進行 == 比較。如果比較為True,則對錯誤進行處理。

通過輸出,我們看到 error 確實是syntax error in pattern

但是這種方式有個問題:就是這些錯誤,往往是提前約定好的,而且處理起來不太靈活。

不過最大的問題是引入了外部包,導致在定義 error 和使用 error 的包之間建立了依賴關係。比如例項中,就引入了path/filepath包。

當然這是標準庫的包,還能接受。如果很多使用者自定義的包都定義了錯誤,那我們就要引入很多包,來判斷各種錯誤,這容易引起迴圈引用的問題。

不過這種比較的優點就是錯誤界限比較清楚,能夠清晰的知道到底是什麼錯誤

2.contains 比較

contains 這種方式的比較,是用字串匹配的方式判斷錯誤字串裡是不是出現了某種錯誤。
例子如下:

func openFile(path string) error {
    _, err := os.Open(path)
    if err != nil {
        return fmt.Errorf("cannot open file, err:", err)
    }
    return nil
}

func main(){
    err := openFile("./test.txt")
    if strings.Contains(error.Error(), "not found") {
        // handle error
    }
}

這種處理方式,給人一種很模糊的感覺,而且程式碼風格怪怪的,error.Error() 是設計用來處理錯誤,結果是要寫到檔案或是列印出來,用上述方式比較則顯得不規範。

通過型別斷言來判斷error是哪種型別的錯誤,通常指的是那些實現了 error 介面的型別。

這些型別一般都是結構體,除了error欄位外,還有其他欄位,提供了額外的資訊。

我們看一個例項:

type PathError struct {
    Op   string
    Path string
    Err  error
}

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

上述程式碼是 PathError型別錯誤的定義及實現。
Error()方法拼接 操作路徑實際錯誤 並返回它。這樣我們就得到了錯誤資訊。

我們驗證一下這些欄位的輸出內容是什麼:

f, err := os.Open("./test.txt")
if err, ok := err.(*os.PathError); ok {
    fmt.Printf("err.Op -> %s \n", err.Op)
    fmt.Printf("err.Path -> %s\n", err.Path)
    fmt.Printf("err.Err -> %v\n", err.Err)
    return
}
fmt.Println(f.Name(), "開啟成功")

輸出:

err.Op -> open
err.Path -> ./test.txt
err.Err -> The system cannot find the file specified.

通常,使用這樣的 error 型別,外層呼叫者需要使用型別斷言來判斷錯誤。

不過錯誤發生並不一定是自己所希望的那樣,具有意外性,如果考慮比較全面,想斷言多種型別的錯誤然後一一處理,會使用很多if elseswitch case語句。

這樣的做的話,無形中會匯入很多外部的包,容易引起迴圈引用,不太推薦。

斷言底層型別的行為,通常指的是呼叫struct型別的方法來獲取更多資訊。

舉個例子,檢視 DNSError 原始碼:

// DNSError represents a DNS lookup error.
type DNSError struct {
    Err         string // description of the error
    Name        string // name looked for
    Server      string // server used
    IsTimeout   bool   // if true, timed out; not all timeouts set this
    IsTemporary bool   // if true, error is temporary; not all errors set this
    IsNotFound  bool   // if true, host could not be found
}

func (e *DNSError) Timeout() bool { return e.IsTimeout }

func (e *DNSError) Temporary() bool { return e.IsTimeout || e.IsTemporary }

從上面的程式碼中可以看到,DNSError有兩個方法Timeout()Temporary(),它們都返回一個布林值,表示錯誤是超時還是臨時的。

實戰一下:

addrs, err := net.LookupHost("www.bucunzaide.com")

if err != nil {
    if ins, ok := err.(*net.DNSError); ok {
        if ins.IsTimeout {
            fmt.Println("連結超時......")
        } else if ins.IsTemporary {
            fmt.Println("暫時性錯誤......")
        } else if ins.IsNotFound {
            fmt.Printf("連結無法找到......,err:%v\n", err)
        } else {
            fmt.Println("未知錯誤......", err)
        }
    }
    return
}
fmt.Println("訪問成功,地址為:", addrs)

輸出:

連結無法找到......,err:lookup www.bucunzaide.com: no such host

例子中隨便造了一個域名,然後去訪問,拿到網路請求返回的 error 後,我們去斷言了錯誤型別,然後去判斷是DNSError的哪種錯誤行為,這樣我們就能知道請求錯誤發生的原因了。

這樣做的好處是不需要 import 引用定義錯誤的包,因為判斷就是結構體的方法(已經引用過包了),比較推薦這種方式。

總結一下,今天主要記錄了處理錯誤的基本三種方式,以後還會一直跟進錯誤處理這個話題,學習了更好更優雅的處理方式後會再記錄。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章