19 | 錯誤處理(上)
提到 Go 語言中的錯誤處理,我們其實已經在前面接觸過幾次了。
比如,我們宣告過error型別的變數err,也呼叫過errors包中的New函式。
我們說過error型別其實是一個介面型別,也是一個 Go 語言的內建型別。在這個介面型別的宣告中只包含了一個方法Error。Error方法不接受任何引數,但是會返回一個string型別的結果。它的作用是返回錯誤資訊的字串表示形式。
我們使用error型別的方式通常是,在函式宣告的結果列表的最後,宣告一個該型別的結果,同時在呼叫這個函式之後,先判斷它返回的最後一個結果值是否“不為nil”。
如果這個值“不為nil”,那麼就進入錯誤處理流程,否則就繼續進行正常的流程。下面是一個例子,程式碼在 demo44.go 檔案中。
package main
import (
"errors"
"fmt"
)
func echo(request string) (response string, err error) {
if request == "" {
err = errors.New("empty request")
return
}
response = fmt.Sprintf("echo: %s", request)
return
}
func main() {
for _, req := range []string{"", "hello!"} {
fmt.Printf("request: %s\n", req)
resp, err := echo(req)
if err != nil {
fmt.Printf("error: %s\n", err)
continue
}
fmt.Printf("response: %s\n", resp)
}
}
我們先看echo函式的宣告。echo函式接受一個string型別的引數request,並會返回兩個結果。
這兩個結果都是有名稱的,第一個結果response也是string型別的,它代表了這個函式正常執行後的結果值。
第二個結果err就是error型別的,它代表了函式執行出錯時的結果值,同時也包含了具體的錯誤資訊。
當echo函式被呼叫時,它會先檢查引數request的值。如果該值為空字串,那麼它就會通過呼叫errors.New函式,為結果err賦值,然後忽略掉後邊的操作並直接返回。
此時,結果response的值也會是一個空字串。如果request的值並不是空字串,那麼它就為結果response賦一個適當的值,然後返回,此時結果err的值會是nil。
再來看main函式中的程式碼。我在每次呼叫echo函式之後,都會把它返回的結果值賦給變數resp和err,並且總是先檢查err的值是否“不為nil”,如果是,就列印錯誤資訊,否則就列印常規的響應資訊。
這裡值得注意的地方有兩個。第一,在echo函式和main函式中,我都使用到了衛述語句。我在前面講函式用法的時候也提到過衛述語句。簡單地講,它就是被用來檢查後續操作的前置條件並進行相應處理的語句。
對於echo函式來說,它進行常規操作的前提是:傳入的引數值一定要符合要求。而對於呼叫echo函式的程式來說,進行後續操作的前提就是echo函式的執行不能出錯。
我們在進行錯誤處理的時候經常會用到衛述語句,以至於有些人會吐槽說:“我的程式滿屏都是衛述語句,簡直是太難看了!”不過,我倒認為這有可能是程式設計上的問題。每個程式語言的理念和風格幾乎都會有明顯的不同,我們常常需要順應它們的紋理去做設計,而不是用其他語言的程式設計思想來編寫當下語言的程式。
再來說第二個值得注意的地方。我在生成error型別值的時候,用到了errors.New函式。
這是一種最基本的生成錯誤值的方式。我們呼叫它的時候傳入一個由字串代表的錯誤資訊,它會給返回給我們一個包含了這個錯誤資訊的error型別值。該值的靜態型別當然是error,而動態型別則是一個在errors包中的,包級私有的型別*errorString。
顯然,errorString型別擁有的一個指標方法實現了error介面中的Error方法。這個方法在被呼叫後,會原封不動地返回我們之前傳入的錯誤資訊。實際上,error型別值的Error方法就相當於其他型別值的String方法。
我們已經知道,通過呼叫fmt.Printf函式,並給定佔位符%s就可以列印出某個值的字串表示形式。
對於其他型別的值來說,只要我們能為這個型別編寫一個String方法,就可以自定義它的字串表示形式。而對於error型別值,它的字串表示形式則取決於它的Error方法。
在上述情況下,fmt.Printf函式如果發現被列印的值是一個error型別的值,那麼就會去呼叫它的Error方法。fmt包中的這類列印函式其實都是這麼做的。
順便提一句,當我們想通過模板化的方式生成錯誤資訊,並得到錯誤值時,可以使用fmt.Errorf函式。該函式所做的其實就是先呼叫fmt.Sprintf函式,得到確切的錯誤資訊;再呼叫errors.New函式,得到包含該錯誤資訊的error型別值,最後返回該值。
好了,我現在問一個關於對錯誤值做判斷的問題。我們今天的問題是:對於具體錯誤的判斷,Go 語言中都有哪些慣用法?
由於error是一個介面型別,所以即使同為error型別的錯誤值,它們的實際型別也可能不同。這個問題還可以換一種問法,即:怎樣判斷一個錯誤值具體代表的是哪一類錯誤?
這道題的典型回答是這樣的:
1、對於型別在已知範圍內的一系列錯誤值,一般使用型別斷言表示式或型別switch語句來判斷;
2、對於已有相應變數且型別相同的一系列錯誤值,一般直接使用判等操作來判斷;
3、對於沒有相應變數且型別未知的一系列錯誤值,只能使用其錯誤資訊的字串表示形式來做判斷。
問題解析
如果你看過一些 Go 語言標準庫的原始碼,那麼對這幾種情況應該都不陌生。我下面分別對它們做個說明。
型別在已知範圍內的錯誤值其實是最容易分辨的。就拿os包中的幾個代表錯誤的型別os.PathError、os.LinkError、os.SyscallError和os/exec.Error來說,它們的指標型別都是error介面的實現型別,同時它們也都包含了一個名叫Err,型別為error介面型別的代表潛在錯誤的欄位。
如果我們得到一個error型別值,並且知道該值的實際型別肯定是它們中的某一個,那麼就可以用型別switch語句去做判斷。例如:
func underlyingError(err error) error {
switch err := err.(type) {
case *os.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
case *exec.Error:
return err.Err
}
return err
}
函式underlyingError的作用是:獲取和返回已知的作業系統相關錯誤的潛在錯誤值。其中的型別switch語句中有若干個case子句,分別對應了上述幾個錯誤型別。當它們被選中時,都會把函式引數err的Err欄位作為結果值返回。如果它們都未被選中,那麼該函式就會直接把引數值作為結果返回,即放棄獲取潛在錯誤值。
只要型別不同,我們就可以如此分辨。但是在錯誤值型別相同的情況下,這些手段就無能為力了。在 Go 語言的標準庫中也有不少以相同方式建立的同型別的錯誤值。
我們還拿os包來說,其中不少的錯誤值都是通過呼叫errors.New函式來初始化的,比如:os.ErrClosed、os.ErrInvalid以及os.ErrPermission,等等。
注意,與前面講到的那些錯誤型別不同,這幾個都是已經定義好的、確切的錯誤值。os包中的程式碼有時候會把它們當做潛在錯誤值,封裝進前面那些錯誤型別的值中。
如果我們在操作檔案系統的時候得到了一個錯誤值,並且知道該值的潛在錯誤值肯定是上述值中的某一個,那麼就可以用普通的switch語句去做判斷,當然了,用if語句和判等操作符也是可以的。例如:
printError := func(i int, err error) {
if err == nil {
fmt.Println("nil error")
return
}
err = underlyingError(err)
switch err {
case os.ErrClosed:
fmt.Printf("error(closed)[%d]: %s\n", i, err)
case os.ErrInvalid:
fmt.Printf("error(invalid)[%d]: %s\n", i, err)
case os.ErrPermission:
fmt.Printf("error(permission)[%d]: %s\n", i, err)
}
}
這個由printError變數代表的函式會接受一個error型別的引數值。該值總會代表某個檔案操作相關的錯誤,這是我故意地以不正確的方式操作檔案後得到的。
雖然我不知道這些錯誤值的型別的範圍,但卻知道它們或它們的潛在錯誤值一定是某個已經在os包中定義的值。
所以,我先用underlyingError函式得到它們的潛在錯誤值,當然也可能只得到原錯誤值而已。然後,我用switch語句對錯誤值進行判等操作,三個case子句分別對應我剛剛提到的那三個已存在於os包中的錯誤值。如此一來,我就能分辨出具體錯誤了。
對於上面這兩種情況,我們都有明確的方式去解決。但是,如果我們對一個錯誤值可能代表的含義知之甚少,那麼就只能通過它擁有的錯誤資訊去做判斷了。
好在我們總是能通過錯誤值的Error方法,拿到它的錯誤資訊。其實os包中就有做這種判斷的函式,比如:os.IsExist、os.IsNotExist和os.IsPermission。命令原始碼檔案 demo45.go 中包含了對它們的應用,這大致跟前面展示的程式碼差不太多,我就不在這裡贅述了。
package main
import (
"fmt"
"os"
"os/exec"
"runtime"
)
// underlyingError 會返回已知的作業系統相關錯誤的潛在錯誤值。
func underlyingError(err error) error {
switch err := err.(type) {
case *os.PathError:
return err.Err
case *os.LinkError:
return err.Err
case *os.SyscallError:
return err.Err
case *exec.Error:
return err.Err
}
return err
}
func main() {
// 示例1。
r, w, err := os.Pipe()
if err != nil {
fmt.Printf("unexpected error: %s\n", err)
return
}
// 人為製造 *os.PathError 型別的錯誤。
r.Close()
_, err = w.Write([]byte("hi"))
uError := underlyingError(err)
fmt.Printf("underlying error: %s (type: %T)\n",
uError, uError)
fmt.Println()
// 示例2。
paths := []string{
os.Args[0], // 當前的原始碼檔案或可執行檔案。
"/it/must/not/exist", // 肯定不存在的目錄。
os.DevNull, // 肯定存在的目錄。
}
printError := func(i int, err error) {
if err == nil {
fmt.Println("nil error")
return
}
err = underlyingError(err)
switch err {
case os.ErrClosed:
fmt.Printf("error(closed)[%d]: %s\n", i, err)
case os.ErrInvalid:
fmt.Printf("error(invalid)[%d]: %s\n", i, err)
case os.ErrPermission:
fmt.Printf("error(permission)[%d]: %s\n", i, err)
}
}
var f *os.File
var index int
{
index = 0
f, err = os.Open(paths[index])
if err != nil {
fmt.Printf("unexpected error: %s\n", err)
return
}
// 人為製造潛在錯誤為 os.ErrClosed 的錯誤。
f.Close()
_, err = f.Read([]byte{})
printError(index, err)
}
{
index = 1
// 人為製造 os.ErrInvalid 錯誤。
f, _ = os.Open(paths[index])
_, err = f.Stat()
printError(index, err)
}
{
index = 2
// 人為製造潛在錯誤為 os.ErrPermission 的錯誤。
_, err = exec.LookPath(paths[index])
printError(index, err)
}
if f != nil {
f.Close()
}
fmt.Println()
// 示例3。
paths2 := []string{
runtime.GOROOT(), // 當前環境下的Go語言根目錄。
"/it/must/not/exist", // 肯定不存在的目錄。
os.DevNull, // 肯定存在的目錄。
}
printError2 := func(i int, err error) {
if err == nil {
fmt.Println("nil error")
return
}
err = underlyingError(err)
if os.IsExist(err) {
fmt.Printf("error(exist)[%d]: %s\n", i, err)
} else if os.IsNotExist(err) {
fmt.Printf("error(not exist)[%d]: %s\n", i, err)
} else if os.IsPermission(err) {
fmt.Printf("error(permission)[%d]: %s\n", i, err)
} else {
fmt.Printf("error(other)[%d]: %s\n", i, err)
}
}
{
index = 0
err = os.Mkdir(paths2[index], 0700)
printError2(index, err)
}
{
index = 1
f, err = os.Open(paths[index])
printError2(index, err)
}
{
index = 2
_, err = exec.LookPath(paths[index])
printError2(index, err)
}
if f != nil {
f.Close()
}
}
總結
今天我們一起初步學習了錯誤處理的內容。我們總結了錯誤型別、錯誤值的處理技巧和設計方式,並一起分享了 Go 語言中處理錯誤的最基本方式
思考題
請列舉出你經常用到或者看到的 3 個錯誤型別,它們所在的錯誤型別體系都是怎樣的?你能畫出一棵樹來描述它們嗎?
筆記原始碼
https://github.com/MingsonZheng/go-core-demo
本作品採用知識共享署名-非商業性使用-相同方式共享 4.0 國際許可協議進行許可。
歡迎轉載、使用、重新發布,但務必保留文章署名 鄭子銘 (包含連結: http://www.cnblogs.com/MingsonZheng/ ),不得用於商業目的,基於本文修改後的作品務必以相同的許可釋出。