天天看點

Go函數--error接口1 Go語言錯誤處理思想2 error接口的定義格式3 錯誤處理的最佳實踐參考

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語言學習筆記》