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 else
或 switch 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 協議》,轉載必須註明作者和本文連結