1 Go語言錯誤處理思想
Go語言的錯誤處理思想和設計原理包含以下特征:
- 一個可能發生錯誤的函數,需要傳回值中傳回一個錯誤接口(error)。如果函數調用是成功的,錯誤接口傳回 nil,否則傳回錯誤。
- 在函數調用後需要檢查錯誤,如果發生錯誤,進行必要的錯誤處理。
<提示> Go語言沒有類似Java、.Net中的異常處理機制,雖然可以使用defer、panic、recover模拟,但是官方并不主張這樣做。Go語言的設計者認為其他語言的異常機制已被過度使用,上層邏輯需要為函數發生的異常付出太多的資源。同時,如果函數使用者覺得錯誤處理很麻煩而忽略錯誤,那麼程式将在不可預知的時刻崩潰。
Go語言希望開發者将錯誤處理視為正常開發必須實作的環節,正确地處理每一個可能發生錯誤的函數。同時,Go語言使用傳回值傳回錯誤的機制,也能大幅降低編譯器、運作時處理錯誤的複雜度,讓開發者真正地掌握錯誤的處理。
2 error接口的定義格式
// src/builtin/builtin.go
type error interface {
Error() string
}
所有符合 Error() string 格式的方法,都能實作錯誤接口。Error()方法傳回錯誤的具體描述資訊,使用者可以通過這字元串知道發生了什麼錯誤。
Go語言标準庫提供了兩個函數傳回實作了 error 接口的具體類型執行個體,一般的錯誤可以使用這兩個函數進行封裝。遇到複雜的錯誤,使用者可以自定義錯誤類型,隻要實作 error 接口的 Error()方法即可。
2.1 errors包 -- errors.New() 方法
// src/errors/errors.go
// 建立錯誤對象
func New(text string) error {
return &errorString{text}
}
// 聲明錯誤字元串結構體
type errorString struct {
s string
}
// 實作error接口的Error()方法,傳回錯誤描述
func (e *errorString) Error() string {
return e.s
}
示例1:除數為0的錯誤。
import (
"errors" //需要導入errors包
"fmt"
)
//定義一個除數為0的錯誤對象
var errDivisionByZero = errors.New("division by zero")
func div(dividend int, divisor int) (int, error){
//判斷除數為0的情況并傳回
if divisor == 0 {
return 0, errDivisionByZero
}
//正常計算,傳回空錯誤
return dividend / divisor, nil
}
func main(){
fmt.Println(div(24, 8)) //3 <nil>
fmt.Println(div(8, 0)) //0 division by zero
}
《代碼說明》當div()函數傳回除數為0的錯誤對象時,它會自動調用該結構體類型已經實作的Error()方法,進而輸出錯誤字元串描述資訊。
2.2 fmt包 -- fmt.Errorf() 方法
// src/fmt/errors.go
func Errorf(format string, a ...interface{}) error
《說明》該fmt.Errorf()函數傳回一個格式化内容的錯誤對象。
示例2:修改上面的代碼,改用fmt.Errorf()函數。
import (
"fmt"
)
func div(dividend int, divisor int) (int, error){
//判斷除數為0的情況并傳回
if divisor == 0 {
errDivisionByZero := fmt.Errorf("division by zero")
return 0, errDivisionByZero
}
//正常計算,傳回空錯誤
return dividend / divisor, nil
}
func main(){
fmt.Println(div(24, 8)) //3 <nil>
fmt.Println(div(8, 0)) //0 division by zero
}
2.3 自定義錯誤類型
使用 errors.New() 定義的錯誤字元串的錯誤類型是無法提供豐富的錯誤資訊的。如果需要攜帶多種錯誤資訊傳回,就需要借助自定義錯誤結構體類型并實作error接口來實作。
示例3:實作一個解析錯誤(ParseError)結構體類型,這個錯誤結構體包含兩個内容:檔案名和行号。解析錯誤結構體需要實作error接口的Error()方法,傳回錯誤描述時,就需要将檔案名和行号傳回。
//聲明一個自定義錯誤類型結構體
type ParseError struct{
Filename string
Line int
}
//實作error接口的Error()方法,傳回錯誤描述
func (e *ParseError) Error() string {
return fmt.Sprintf("%s:%d", e.Filename, e.Line)
}
//建立解析錯誤函數,傳回的是結構體對象指針
func newParseError(filename string, line int) error{
return &ParseError{filename, line}
}
func main(){
var e error
//建立一個錯誤執行個體,包含檔案名和行号
e = newParseError("demo.go", 10)
//通過error接口檢視錯誤資訊
fmt.Println(e.Error())
//根據錯誤接口的具體類型,擷取詳細的錯誤資訊
switch detail := e.(type){
case *ParseError:
fmt.Printf("Filename: %s, Line: %d\n", detail.Filename, detail.Line)
default:
fmt.Println("other error")
}
}
運作結果:
demo.go:10
Filename: demo.go, Line: 10
<提示> 自定義錯誤結構體類型都要實作error 接口的Error()方法,這樣,所有的錯誤都可以獲得字元串的描述。如果想進一步知道錯誤的詳細資訊,可以通過類型斷言,将錯誤對象轉換為具體的錯誤類型進行錯誤詳細資訊的擷取。
3 錯誤處理的最佳實踐
1、在多個傳回值的函數中,error 通常作為函數最後一個傳回值。
2、如果一個函數傳回 error類型變量,則先用if 語句處理 error != nil 的異常情況,正常邏輯放到if 語句塊的後面,保持代碼平坦。
3、defer 語句應該放到 err 判斷的後面,不然有可能産生 panic。示例代碼如下:
// 正确寫法
f, err := os.Open("defer.txt")
if err != nil {
return nil, errors.New("Open file failed")
}
defer f.Close()
// 錯誤寫法
f, err := os.Open("defer.txt")
defer f.Close()
if err != nil {
return nil, errors.New("Open file failed")
}
// 如果f為空,就不能調用f.Close()函數了,會直接導緻程式panic。
4、在錯誤逐級向上層傳遞的過程中,錯誤資訊應該不斷地豐富和完善,而不是簡單地抛出下層調用的錯誤。這在錯誤日志分析時非常有用和友好。
參考
《Go語言從入門到進階實戰(視訊教學版)》
《Go語言核心程式設計》
《Go語言學習筆記》