Go 經(jīng)典入門系列 30:錯誤處理
歡迎來到 Golang 系列教程[1]的第 30 篇。
什么是錯誤?
錯誤表示程序中出現(xiàn)了異常情況。比如當我們試圖打開一個文件時,文件系統(tǒng)里卻并沒有這個文件。這就是異常情況,它用一個錯誤來表示。
在 Go 中,錯誤一直是很常見的。錯誤用內(nèi)建的 error 類型來表示。
就像其他的內(nèi)建類型(如 int、float64 等),錯誤值可以存儲在變量里、作為函數(shù)的返回值等等。
示例
現(xiàn)在我們開始編寫一個示例,該程序試圖打開一個并不存在的文件。
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] 函數(shù)有如下簽名:
func?Open(name?string)?(file?*File,?err?error)
如果成功打開文件,Open 函數(shù)會返回一個文件句柄(File Handler)和一個值為 nil 的錯誤。而如果打開文件時發(fā)生了錯誤,會返回一個不等于 nil 的錯誤。
如果一個函數(shù)[4] 或方法[5] 返回了錯誤,按照慣例,錯誤會作為最后一個值返回。于是 Open 函數(shù)也是將 err 作為最后一個返回值。
按照 Go 的慣例,在處理錯誤時,通常都是將返回的錯誤與 nil 比較。nil 值表示了沒有錯誤發(fā)生,而非 nil 值表示出現(xiàn)了錯誤。在這里,我們第 10 行檢查了錯誤值是否為 nil。如果不是 nil,我們會簡單地打印出錯誤,并在 main 函數(shù)中返回。
運行該程序會輸出:
open?/test.txt:?No?such?file?or?directory
很棒!我們得到了一個錯誤,它指出該文件并不存在。
錯誤類型的表示
讓我們進一步深入,理解 error 類型是如何定義的。error 是一個接口[6]類型,定義如下:
type?error?interface?{
????Error()?string
}
error 有了一個簽名為 Error() string 的方法。所有實現(xiàn)該接口的類型都可以當作一個錯誤類型。Error() 方法給出了錯誤的描述。
fmt.Println 在打印錯誤時,會在內(nèi)部調(diào)用 Error() string 方法來得到該錯誤的描述。上一節(jié)示例中的第 11 行,就是這樣打印出錯誤的描述的。
從錯誤獲取更多信息的不同方法
現(xiàn)在,我們知道了 error 是一個接口類型,讓我們看看如何從一個錯誤獲取更多信息。
在前面的示例里,我們只是打印出錯誤的描述。如果我們想知道這個錯誤的文件路徑,該怎么做呢?一種選擇是直接解析錯誤的字符串。這是前面示例的輸出:
open?/test.txt:?No?such?file?or?directory
我們解析了這條錯誤信息,雖然獲取了發(fā)生錯誤的文件路徑,但是這種方法很不優(yōu)雅。隨著語言版本的更新,這條錯誤的描述隨時都有可能變化,使我們程序出錯。
有沒有更加可靠的方法來獲取文件名呢?答案是肯定的,這是可以做到的,Go 標準庫給出了各種提取錯誤相關(guān)信息的方法。我們一個個來看看吧。
1. 斷言底層結(jié)構(gòu)體類型,使用結(jié)構(gòu)體字段獲取更多信息
如果你仔細閱讀了 `Open`[7] 函數(shù)的文檔,你可以看見它返回的錯誤類型是 *PathError。`PathError`[8] 是結(jié)構(gòu)體[9]類型,它在標準庫中的實現(xiàn)如下:
type?PathError?struct?{
????Op???string
????Path?string
????Err??error
}
func?(e?*PathError)?Error()?string?{?return?e.Op?+?"?"?+?e.Path?+?":?"?+?e.Err.Error()?}
如果你有興趣了解上述源代碼出現(xiàn)的位置,可以在這里找到:https://golang.org/src/os/error.go?s=653:716#L11。
通過上面的代碼,你就知道了 *PathError 通過聲明 Error() string 方法,實現(xiàn)了 error 接口。Error() string 將文件操作、路徑和實際錯誤拼接,并返回該字符串。于是我們得到該錯誤信息:
open?/test.txt:?No?such?file?or?directory
結(jié)構(gòu)體 PathError 的 Path 字段,就有導(dǎo)致錯誤的文件路徑。我們修改前面寫的程序,打印出該路徑。
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
很棒!我們已經(jīng)使用類型斷言成功獲取到了該錯誤的文件路徑。
2. 斷言底層結(jié)構(gòu)體類型,調(diào)用方法獲取更多信息
第二種獲取更多錯誤信息的方法,也是對底層類型進行斷言,然后通過調(diào)用該結(jié)構(gòu)體類型的方法,來獲取更多的信息。
我們通過一個實例來理解這一點。
標準庫中的 DNSError 結(jié)構(gòu)體類型定義如下:
type?DNSError?struct?{
????...
}
func?(e?*DNSError)?Error()?string?{
????...
}
func?(e?*DNSError)?Timeout()?bool?{
????...
}
func?(e?*DNSError)?Temporary()?bool?{
????...
}
從上述代碼可以看到,DNSError 結(jié)構(gòu)體還有 Timeout() bool 和 Temporary() bool 兩個方法,它們返回一個布爾值,指出該錯誤是由超時引起的,還是臨時性錯誤。
接下來我們編寫一個程序,斷言 *DNSError 類型,并調(diào)用這些方法來確定該錯誤是臨時性錯誤,還是由超時導(dǎo)致的。
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
如果該錯誤是臨時性錯誤,或是由超時引發(fā)的,那么對應(yīng)的 if 語句會執(zhí)行,于是我們就可以適當?shù)靥幚硭鼈儭?/p>
3. 直接比較
第三種獲取錯誤的更多信息的方式,是與 error 類型的變量直接比較。我們通過一個示例來理解。
filepath 包中的 `Glob`[12] 用于返回滿足 glob 模式的所有文件名。如果模式寫的不對,該函數(shù)會返回一個錯誤 ErrBadPattern。
filepath 包中的 ErrBadPattern 定義如下:
var?ErrBadPattern?=?errors.New("syntax?error?in?pattern")
errors.New() 用于創(chuàng)建一個新的錯誤。我們會在下一教程中詳細討論它。
當模式不正確時,Glob 函數(shù)會返回 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 相比較。如果該條件滿足,那么該錯誤就是由模式錯誤導(dǎo)致的。該程序會輸出:
syntax?error?in?pattern
標準庫在提供錯誤的詳細信息時,使用到了上述提到的三種方法。在下一教程里,我們會通過這些方法來創(chuàng)建我們自己的自定義錯誤。
不可忽略錯誤
絕不要忽略錯誤。忽視錯誤會帶來問題。接下來我重寫上面的示例,在列出所有滿足模式的文件名時,我省略了錯誤處理的代碼。
package?main
import?(
????"fmt"
????"path/filepath"
)
func?main()?{
????files,?_?:=?filepath.Glob("[")
????fmt.Println("matched?files",?files)
}
在 playground 上運行[14]
我們已經(jīng)從前面的示例知道了這個模式是錯誤的。在第 9 行,通過使用 _ 空白標識符,我忽略了 Glob 函數(shù)返回的錯誤。我在第 10 行簡單打印了所有匹配的文件。該程序會輸出:
matched?files?[]
由于我忽略了錯誤,輸出看起來就像是沒有任何匹配了 glob 模式的文件,但實際上這是因為模式的寫法不對。所以絕不要忽略錯誤。
本教程到此結(jié)束。
這一教程我們討論了該如何處理程序中出現(xiàn)的錯誤,也討論了如何查詢關(guān)于錯誤的更多信息。簡單概括一下本教程討論的內(nèi)容:
什么是錯誤? 錯誤的表示 獲取錯誤詳細信息的各種方法 不能忽視錯誤
在下一教程,我們會創(chuàng)建我們自己的自定義錯誤,并給標準錯誤增加更多的語境(Context)。
祝你愉快。
下一教程 - 自定義錯誤
via: https://golangbot.com/error-handling/
作者:Nick Coghlan[15]譯者:Noluye[16]校對:polaris1119[17]
本文由 GCTT[18] 原創(chuàng)編譯,Go 中文網(wǎng)[19] 榮譽推出
參考資料
Golang 系列教程: https://studygolang.com/subject/2
[2]在 playground 中運行: https://play.golang.org/p/yOhAviFM05
[3]Open: https://golang.org/pkg/os/#Open
函數(shù): 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
PathError: https://golang.org/pkg/os/#PathError
結(jié)構(gòu)體: 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
在 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 中文網(wǎng): https://studygolang.com/
推薦閱讀
