歡迎來到 Golang 系列教程[1]的第 30 篇。
什麼是錯誤?錯誤表示程序中出現了異常情況。比如當我們試圖打開一個文件時,文件系統裡卻並沒有這個文件。這就是異常情況,它用一個錯誤來表示。
在 Go 中,錯誤一直是很常見的。錯誤用內建的 error 類型來表示。
就像其他的內建類型(如 int、float64 等),錯誤值可以存儲在變量裡、作為函數的返回值等等。
示例現在我們開始編寫一個示例,該程序試圖打開一個並不存在的文件。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err != nil {
fmt.Println(err)
return
}
fmt.Println(f.Name(), "opened successfully")
}在 playground 中運行[2]
在程序的第 9 行,我們試圖打開路徑為 /test.txt 的文件(playground 顯然並不存在這個文件)。os 包裡的 `Open`[3] 函數有如下簽名:
func Open(name string) (file *File, err error)如果成功打開文件,Open 函數會返回一個文件句柄(File Handler)和一個值為 nil 的錯誤。而如果打開文件時發生了錯誤,會返回一個不等於 nil 的錯誤。
如果一個函數[4] 或方法[5] 返回了錯誤,按照慣例,錯誤會作為最後一個值返回。於是 Open 函數也是將 err 作為最後一個返回值。
按照 Go 的慣例,在處理錯誤時,通常都是將返回的錯誤與 nil 比較。nil 值表示了沒有錯誤發生,而非 nil 值表示出現了錯誤。在這裡,我們第 10 行檢查了錯誤值是否為 nil。如果不是 nil,我們會簡單地列印出錯誤,並在 main 函數中返回。
運行該程序會輸出:
open /test.txt: No such file or directory很棒!我們得到了一個錯誤,它指出該文件並不存在。
錯誤類型的表示讓我們進一步深入,理解 error 類型是如何定義的。error 是一個接口[6]類型,定義如下:
type error interface {
Error() string
}error 有了一個籤名為 Error() string 的方法。所有實現該接口的類型都可以當作一個錯誤類型。Error() 方法給出了錯誤的描述。
fmt.Println 在列印錯誤時,會在內部調用 Error() string 方法來得到該錯誤的描述。上一節示例中的第 11 行,就是這樣列印出錯誤的描述的。
從錯誤獲取更多信息的不同方法現在,我們知道了 error 是一個接口類型,讓我們看看如何從一個錯誤獲取更多信息。
在前面的示例裡,我們只是列印出錯誤的描述。如果我們想知道這個錯誤的文件路徑,該怎麼做呢?一種選擇是直接解析錯誤的字符串。這是前面示例的輸出:
open /test.txt: No such file or directory我們解析了這條錯誤信息,雖然獲取了發生錯誤的文件路徑,但是這種方法很不優雅。隨著語言版本的更新,這條錯誤的描述隨時都有可能變化,使我們程序出錯。
有沒有更加可靠的方法來獲取文件名呢?答案是肯定的,這是可以做到的,Go 標準庫給出了各種提取錯誤相關信息的方法。我們一個個來看看吧。
1. 斷言底層結構體類型,使用結構體欄位獲取更多信息如果你仔細閱讀了 `Open`[7] 函數的文檔,你可以看見它返回的錯誤類型是 *PathError。`PathError`[8] 是結構體[9]類型,它在標準庫中的實現如下:
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Error() string { return e.Op + " " + e.Path + ": " + e.Err.Error() }如果你有興趣了解上述原始碼出現的位置,可以在這裡找到:https://golang.org/src/os/error.go?s=653:716#L11。
通過上面的代碼,你就知道了 *PathError 通過聲明 Error() string 方法,實現了 error 接口。Error() string 將文件操作、路徑和實際錯誤拼接,並返回該字符串。於是我們得到該錯誤信息:
open /test.txt: No such file or directory結構體 PathError 的 Path 欄位,就有導致錯誤的文件路徑。我們修改前面寫的程序,列印出該路徑。
package main
import (
"fmt"
"os"
)
func main() {
f, err := os.Open("/test.txt")
if err, ok := err.(*os.PathError); ok {
fmt.Println("File at path", err.Path, "failed to open")
return
}
fmt.Println(f.Name(), "opened successfully")
}在 playground 上運行[10]
在上面的程序裡,我們在第 10 行使用了類型斷言[11](Type Assertion)來獲取 error 接口的底層值(Underlying Value)。接下來在第 11 行,我們使用 err.Path 來列印該路徑。該程序會輸出:
File at path /test.txt failed to open很棒!我們已經使用類型斷言成功獲取到了該錯誤的文件路徑。
2. 斷言底層結構體類型,調用方法獲取更多信息第二種獲取更多錯誤信息的方法,也是對底層類型進行斷言,然後通過調用該結構體類型的方法,來獲取更多的信息。
我們通過一個實例來理解這一點。
標準庫中的 DNSError 結構體類型定義如下:
type DNSError struct {
...
}
func (e *DNSError) Error() string {
...
}
func (e *DNSError) Timeout() bool {
...
}
func (e *DNSError) Temporary() bool {
...
}從上述代碼可以看到,DNSError 結構體還有 Timeout() bool 和 Temporary() bool 兩個方法,它們返回一個布爾值,指出該錯誤是由超時引起的,還是臨時性錯誤。
接下來我們編寫一個程序,斷言 *DNSError 類型,並調用這些方法來確定該錯誤是臨時性錯誤,還是由超時導致的。
package main
import (
"fmt"
"net"
)
func main() {
addr, err := net.LookupHost("golangbot123.com")
if err, ok := err.(*net.DNSError); ok {
if err.Timeout() {
fmt.Println("operation timed out")
} else if err.Temporary() {
fmt.Println("temporary error")
} else {
fmt.Println("generic error: ", err)
}
return
}
fmt.Println(addr)
}註:在 playground 無法進行 DNS 解析。請在你的本地運行該程序。
在上述程序中,我們在第 9 行,試圖獲取 golangbot123.com(無效的域名) 的 ip。在第 10 行,我們通過 *net.DNSError 的類型斷言,獲取到了錯誤的底層值。接下來的第 11 行和第 13 行,我們分別檢查了該錯誤是由超時引起的,還是一個臨時性錯誤。
在本例中,我們的錯誤既不是臨時性錯誤,也不是由超時引起的,因此該程序輸出:
generic error: lookup golangbot123.com: no such host如果該錯誤是臨時性錯誤,或是由超時引發的,那麼對應的 if 語句會執行,於是我們就可以適當地處理它們。
3. 直接比較第三種獲取錯誤的更多信息的方式,是與 error 類型的變量直接比較。我們通過一個示例來理解。
filepath 包中的 `Glob`[12] 用於返回滿足 glob 模式的所有文件名。如果模式寫的不對,該函數會返回一個錯誤 ErrBadPattern。
filepath 包中的 ErrBadPattern 定義如下:
var ErrBadPattern = errors.New("syntax error in pattern")errors.New() 用於創建一個新的錯誤。我們會在下一教程中詳細討論它。
當模式不正確時,Glob 函數會返回 ErrBadPattern。
我們來寫一個小程序來看看這個錯誤。
package main
import (
"fmt"
"path/filepath"
)
func main() {
files, error := filepath.Glob("[")
if error != nil && error == filepath.ErrBadPattern {
fmt.Println(error)
return
}
fmt.Println("matched files", files)
}在 playground 上運行[13]
在上述程序裡,我們查詢了模式為 [ 的文件,然而這個模式寫的不正確。我們檢查了該錯誤是否為 nil。為了獲取該錯誤的更多信息,我們在第 10 行將 error 直接與 filepath.ErrBadPattern 相比較。如果該條件滿足,那麼該錯誤就是由模式錯誤導致的。該程序會輸出:
syntax error in pattern標準庫在提供錯誤的詳細信息時,使用到了上述提到的三種方法。在下一教程裡,我們會通過這些方法來創建我們自己的自定義錯誤。
不可忽略錯誤絕不要忽略錯誤。忽視錯誤會帶來問題。接下來我重寫上面的示例,在列出所有滿足模式的文件名時,我省略了錯誤處理的代碼。
package main
import (
"fmt"
"path/filepath"
)
func main() {
files, _ := filepath.Glob("[")
fmt.Println("matched files", files)
}在 playground 上運行[14]
我們已經從前面的示例知道了這個模式是錯誤的。在第 9 行,通過使用 _ 空白標識符,我忽略了 Glob 函數返回的錯誤。我在第 10 行簡單列印了所有匹配的文件。該程序會輸出:
matched files []由於我忽略了錯誤,輸出看起來就像是沒有任何匹配了 glob 模式的文件,但實際上這是因為模式的寫法不對。所以絕不要忽略錯誤。
本教程到此結束。
這一教程我們討論了該如何處理程序中出現的錯誤,也討論了如何查詢關於錯誤的更多信息。簡單概括一下本教程討論的內容:
在下一教程,我們會創建我們自己的自定義錯誤,並給標準錯誤增加更多的語境(Context)。
祝你愉快。
上一教程 - Defer
下一教程 - 自定義錯誤
via: https://golangbot.com/error-handling/
作者:Nick Coghlan[15]譯者:Noluye[16]校對:polaris1119[17]
本文由 GCTT[18] 原創編譯,Go 中文網[19] 榮譽推出
參考資料[1]Golang 系列教程: https://studygolang.com/subject/2
[2]在 playground 中運行: https://play.golang.org/p/yOhAviFM05
[3]Open: https://golang.org/pkg/os/#Open
[4]函數: https://studygolang.com/articles/11892
[5]方法: https://studygolang.com/articles/12264
[6]接口: https://studygolang.com/articles/12266
[7]Open: https://golang.org/pkg/os/#OpenFile
[8]PathError: https://golang.org/pkg/os/#PathError
[9]結構體: https://studygolang.com/articles/12263
[10]在 playground 上運行: https://play.golang.org/p/JQrqWU7Jf9
[11]類型斷言: https://studygolang.com/articles/12266
[12]Glob: https://golang.org/pkg/path/filepath/#Glob
[13]在 playground 上運行: https://play.golang.org/p/zbVDDHnMZU
[14]在 playground 上運行: https://play.golang.org/p/2k8r_Qg_lc
[15]Nick Coghlan: https://golangbot.com/about/
[16]Noluye: https://github.com/Noluye
[17]polaris1119: https://github.com/polaris1119
[18]GCTT: https://github.com/studygolang/GCTT
[19]Go 中文網: https://studygolang.com/