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