原文地址
golang 中的錯誤處理的哲學和 C 語言一樣,函數通過返回錯誤類型(error)或者 bool 類型(不需要區分多種錯誤狀態時)表明函數的執行結果,調用檢查返回的錯誤類型值是否是 nil 來判斷調用結果。
error
golang 中內置的錯誤類型 error 是一個接口類型,自定義的錯誤類型也必須實現為 error 接口,這樣調用總是可以通過 Error() 獲取到具體的錯誤信息而不用關心錯誤的具體類型。標准庫的 fmt.Errorf 和 errors.New 可以方便的創建 error 類型的變量。
type error interface {
Error() string
}
C 語言的語法限制函數只能有一個返回值,函數的執行結果和執行成功時需要返回的信息都放到這個返回值里,具體的錯誤信息需要調用額外的接口獲取。比如 C 標准庫函數讀取文件的函數 read 返回 -1 時表示讀取錯誤,具體錯誤信息需要通過 errno 獲取,返回 0 時,表示 EOF ,返回正整數時,表示成功讀取到的字節數。golang 的多返回值語法糖避免了這種方式帶來的不便,錯誤值一般作為返回值列表的最后一個,其他返回值是成功執行時需要返回的信息。為了避免錯誤處理時過深的代碼縮進:
if err != nil {
// error handling
} else {
// normal code
}
推薦在發生錯誤時立即返回:
if err != nil {
// error handling
return // or continue, etc.
}
// normal code
雖然這種錯誤處理方式代碼寫起來會有一些冗長,但 golang 風格的代碼應該盡量使用這種方式。
預定義錯誤值
下面的示例代碼中,當需要返回錯誤時,每次都調用 errors.New() 返回一個 error 對象:
func doStuff() error {
if someCondition {
return errors.New("no space left on the device")
} else {
return errors.New("permission denied")
}
}
這種做法的問題是當需要對特定錯誤進行處理時,不方便對錯誤值進行等值判斷。只能用 Error() 取出字符串比較,這樣即不優雅也容易出現拼寫錯誤。最佳的做法是預定義全局的錯誤值:
var ErrNoSpaceLeft = errors.New("no space left on the device")
var ErrPermissionDenied = errors.New("permission denied")
func doStuff() error {
if someCondition {
return ErrNoSpaceLeft
} else {
return ErrPermissionDenied
}
}
這樣錯誤值判斷就方便多了:
if err == ErrNoSpaceLeft {
// handle this particular error
}
標准庫中也預定義了一些錯誤值 ,最常用的就是 io.EOF 。
自定義錯誤類型
HTTP 表示客戶端的錯誤狀態碼有幾十個。如果為每種狀態碼都預定義相應的錯誤值,代碼會變得很繁瑣:
var ErrBadRequest = errors.New("status code 400: bad request")
var ErrUnauthorized = errors.New("status code 401: unauthorized")
// ...
這種場景下最佳的最法是自定義一種錯誤類型,並且至少實現 Error() 方法(滿足 error 定義):
type HTTPError struct {
Code int
Description string
}
func (h *HTTPError) Error() string {
return fmt.Sprintf("status code %d: %s", h.Code, h.Description)
}
這種方式下進行等值判斷時需要轉成具體的自定義類型然后取出 Code 字段判斷:
func request() error {
return &HTTPError{404, "not found"}
}
func main() {
err := request()
if err != nil {
// an error occured
if err.(*HTTPError).Code == 404 {
// handle a "not found" error
} else {
// handle a different error
}
}
}
自定義錯誤類型可以提供更多特定的錯誤信息,標准庫中也有一個這樣的例子 os.PathError。
panic
golang 新手很容易把 panic 當成其他語言的異常(exception)導致 panic 的濫用。這里解釋了為什么 golang 沒有提供類似其他語言的異常機制的原因:主要是 try / catch 的方式處理錯誤過於復雜。使用 panic / recover 的機制也可以達到類似 try / catch 的效果,但是要特別謹慎的使用。
panic / recover 和 try / catch 機制最大的不同在於控制流程上的區別。try / catch 機制控制流作用在 try 代碼塊內,代碼塊執行到異常拋出點(throw)時,控制流跳出 try 代碼塊,轉到對應的 catch 代碼塊,然后繼續往下執行。panic / recover 機制控制流則作用在整個 goroutine 的調用棧。當 goroutine 執行到 panic 時,控制流開始在當前 goroutine 的調用棧內向上回溯(unwind)並執行每個函數的 defer 。如果 defer 中遇到 recover 則回溯停止,如果執行到 goroutine 最頂層的 defer 還沒有 recover ,運行時就輸出調用棧信息然后退出。所以如果要使用 recover 避免 panic 導致進程掛掉,recover 必須要放到 defer 里。
recover 調用的位置如果不對是無法將 panic 恢復的,這里有個比較復雜的規則,具體可以參考這里。為了避免過於復雜的代碼,最好不要使用嵌套的 defer ,並且 recover 應該直接放到 defer 函數里直接調用。
panic 主要用於以下場景:
- 發生嚴重錯誤(critical error)必須讓進程退出。這里“嚴重”判斷的標准是錯誤無法恢復導致程序無法執行或者繼續執行會發生不可預期的行為。有點類似如 C 語言的 assert 機制。標准庫中有些函數也是使用這種機制,比如
regexp.MustCompile。當傳入的參數不是一個合法的正則表達式時,繼續執行已經沒有任何意義。另外一些場景,比如程序啟動時依賴的數據庫不存在或者依賴的配置不可讀取,這個時候如果繼續執行可能會導致發生不可預期的行為,這個時候使用 panic 讓進程直接退出將問題暴露反而是更可取的做法。非“嚴重”的錯誤比如客戶端不合法的請求參數應該返回 error 然后讓調用者去處理而不是使用 panic 讓進程退出。 - 快速退出錯誤處理。也就是上文中提到的模擬 try / catch 的行為。大多數情況下錯誤處理應該使用 error 機制,但有時候函數調用棧很深,逐層返回錯誤可能需要寫很多冗余代碼,這個時候可以使用 panic 讓程序的控制流直接跳到頂層的 recover 處來處理錯誤。這種場景要特別注意在包內的 panic 必須在包內就要 recover 。讓 panic 跨包傳遞可能會導致更復雜的問題,所以包的導出函數不應該產生 panic (上述 1 中的場景除外)。
總結
本文簡要介紹了 golang 的錯誤處理與異常機制。error 是 golang 中錯誤處理的主要方式,golang 程序應該盡量使用 error 來進行錯誤處理。當需要對 error 進行等值判斷以針對特定的錯誤類型進行處理處理時,預定義全局的錯誤值是比較優雅的方式。當需要提供更多額外的錯誤信息時,可以自定義錯誤類型,但至少要實現 Error() 方法以滿足 error 的定義。golang 雖然也有類似其他語言的異常的 panic ,但是使用場景很有限,如非必要,不要使用。
參考資料
- https://stackoverflow.com/questions/3413389/panic-recover-in-go-v-s-try-catch-in-other-languages
- https://stackoverflow.com/questions/44504354/should-i-use-panic-or-return-error
- http://www.golangbootcamp.com/book/tricks_and_tips#sec-using_errors
- https://leanpub.com/GoNotebook/read#leanpub-auto-exceptions
- https://justinas.org/best-practices-for-errors-in-go
