Golang 學習——error 和建立 error 原始碼解析

相守之路發表於2020-05-10

Golang中error和建立error原始碼解析

Golang中的錯誤處理和Java,Python有很大不同,沒有try...catch語句來處理錯誤。因此,Golang中的錯誤處理是一個比較有爭議的點,如何優雅正確的處理錯誤是值得去深究的。

今天先記錄error是什麼及如何建立error,擼一擼原始碼。

1.什麼是error

error錯誤指的是可能出現問題的地方出現了問題。比如開啟一個檔案時失敗,這種情況在人們的意料之中 。

而異常指的是不應該出現問題的地方出現了問題。比如引用了空指標,這種情況在人們的意料之外。

可見,錯誤是業務過程的一部分,而異常不是 。

Golang中的錯誤也是一種型別。錯誤用內建的error型別表示。就像其他型別,如intfloat64等。

錯誤值可以儲存在變數中,也可以從函式中返回,等等。

2.error原始碼

src/builtin/builtin.go 檔案下,定義了錯誤型別,原始碼如下:

// src/builtin/builtin.go

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.
type error interface {
    Error() string
}

error是一個介面型別,它包含一個 Error() 方法,返回值為string。任何實現這個介面的型別都可以作為一個錯誤使用,Error這個方法提供了對錯誤的描述。

注意:error為nil代表沒有錯誤

先看一個檔案開啟錯誤的例子:

f, err := os.Open("/test.txt")
if err != nil {
    fmt.Println("open failed, err:", err)
    return
}
fmt.Println("file is :", f)

輸出:

open failed, err: open /test.txt: The system cannot find the file specified.

可以看到輸出了具體錯誤,分別為: 操作open,操作物件/test.txt,錯誤原因The system cannot find the file specified.

當執行列印錯誤語句時, fmt 包會自動呼叫 err.Error() 函式來列印字串。

這就是錯誤描述是如何在一行中列印出來的原因。

瞭解了error是什麼,我們接下來了解error的建立。

建立方式有兩種:

  • errors.New()
  • fmt.Errorf()

1.errors.New()函式

src/errors/errors.go檔案下,定義了 errors.New()函式,入參為字串,返回一個error物件:

// src/errors/errors.go

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

New()函式返回一個錯誤,該錯誤的格式為給定的文字。
即使文字相同,每次對New的呼叫也會返回一個不同的錯誤值。

其中 errorString是一個結構體,只有一個string型別的欄位s,並且實現了唯一的方法:Error()

我們實戰一下:

// 1.errors.New() 建立一個 error
err1 := errors.New("這是 errors.New() 建立的錯誤")
fmt.Printf("err1 錯誤型別:%T,錯誤為:%v\n", err1, err1)

輸出:

err1 錯誤型別:*errors.errorString,錯誤為:這是 errors.New() 建立的錯誤

可以看到,錯誤型別是 errorString指標,前面的errors.表明了其在errors包下。

通常這就夠了,它能反映當時“出錯了”,但是有些時候我們需要更加具體的資訊。即需要具體的“上下文”資訊,表明具體的錯誤值。

這就用到了fmt.Errorf 函式

2.fmt.Errorf()函式

fmt.Errorf()函式,它先將字串格式化,並增加上下文的資訊,更精確的描述錯誤。

我們先實戰一下,看看和上一節的內容有什麼不同:

// 2.fmt.Errorf()
err2 := fmt.Errorf("這個 fmt.Errorf() 建立的錯誤,錯誤編碼為:%d", 404)
fmt.Printf("err2 錯誤型別:%T,錯誤為:%v\n", err2, err2)

輸出:

err2 錯誤型別:*errors.errorString,錯誤為:這個 fmt.Errorf() 建立的錯誤,錯誤編碼為:404

可以看到err2的型別是*errors.errorString,並且錯誤編碼 404 也輸出了。。

為什麼err2返回的錯誤型別也是 :*errors.errorString,我們不是用 fmt.Errorf()建立的嗎?

我們先看下其原始碼實現:

// src/fmt/errors.go

func Errorf(format string, a ...interface{}) error {
    p := newPrinter()
    p.wrapErrs = true
    p.doPrintf(format, a)
    s := string(p.buf)
    var err error
    if p.wrappedErr == nil {
        err = errors.New(s)
    } else {
        err = &wrapError{s, p.wrappedErr}
    }
    p.free()
    return err
}

通過原始碼可以看到,p.wrappedErrnil的時候,會呼叫errors.New()來建立錯誤。

所以 err2的錯誤型別是*errors.errorString這個問題就解答了。

不過又出現了新問題,這個p.wrappedErr是什麼東東呢?什麼時候為nil?

我們先看個例子:

// 3. go 1.13 新增加的錯誤處理特性  %w
err3 := fmt.Errorf("err3: %w", err2)  // err3包裹err2錯誤
fmt.Printf("err3 錯誤型別:%T,錯誤為:%v\n", err3, err3)

輸出:

err3 錯誤型別:*fmt.wrapError,錯誤為:err3: 這個 fmt.Errorf() 建立的錯誤,錯誤編碼為:404

注意:在格式化字串的時候,有一個 %w佔位符,表示格式化的內容是一個error型別。

我們主要看下err3的內容,其包裹了err2錯誤資訊,如下:

err3: 這個 fmt.Errorf() 建立的錯誤,錯誤編碼為:404

還有一點要注意的是,err3這次是一個 *fmt.wrapError型別?這個型別又是源自哪裡?怎麼會有這樣一個型別?又出現了一個新的問題……

好了,帶著這些問題,我們從頭開始捋一捋原始碼,就知道它們到底是什麼?

我們注意到fmt.Errorf()函式第一行 p := newPrinter()建立了一個 p物件,這個p物件其實就是pp結構體指標的例項, newPrinter()原始碼如下:

// src/fmt/print.go

// newPrinter allocates a new pp struct or grabs a cached one.
func newPrinter() *pp {
    p := ppFree.Get().(*pp)
    p.panicking = false
    p.erroring = false
    p.wrapErrs = false
    p.fmt.init(&p.buf)
    return p
}

newPrinter()函式返回一個 pp結構體指標。

我們看下這個結構體,並看看p.wrappedErr欄位在該結構體中定義:

// src/fmt/print.go

// pp is used to store a printer's state and is reused with sync.Pool to avoid allocations.
type pp struct {
    ...
    ...
    // wrapErrs is set when the format string may contain a %w verb.
    wrapErrs bool

    // wrappedErr records the target of the %w verb.
    wrappedErr error
}

由於pp結構體的欄位較多,我們主要看兩個欄位:

  • wrapErrs欄位,bool型別,當格式字串包含%w動詞時,將賦值為true
  • wrappedErr欄位,error型別,記錄%w動詞的目標,即例子的err2

所以我們解決了第一問題:p.wrappedErr到底是什麼,什麼時候為nil

即:p.wrappedErr是 pp 結構體的一個欄位,當格式化錯誤字串中沒有%w動詞時,其為nil

還有第二個問題, *fmt.wrapError型別源自哪裡?

其實根源就在else語句中,當p.wrappedErr不為nil時,執行以下語句:

err = &wrapError{s, p.wrappedErr}

err 是結構體wrapError的例項,其初始化了兩個欄位,並且是引用取值(前面有&)。我們來看看wrapError原始碼:

// src/fmt/errors.go

type wrapError struct {
    msg string
    err error
}

func (e *wrapError) Error() string {
    return e.msg
}

func (e *wrapError) Unwrap() error {
    return e.err
}

wrapError結構體有兩個欄位:

  • msg ,string型別
  • err,error型別

實現了兩個方法:

  • Error(),也說明wrapError結構體實現了 error介面,是一個error型別
  • Unwrap(),作用是返回原錯誤值,沒有自定義的msg了。也就是說拆開了一個被包裝的 error。

所以我們的第二個問題, *fmt.wrapError是什麼,就徹底解答了。

至此,捋完fmt.Errorf()的原始碼了,我們瞭解了想要的內容,至於p.doPrintf(format, a)的具體實現內容很複雜,所以就沒去深挖了。

總結一下吧,Golang中建立錯誤有兩種方式:
第一種errors.New()函式,其返回值型別為 *errors.errorString

第二種fmt.Errorf()函式
當使用fmt.Errorf()來建立錯誤時,核心有以下兩點:

  1. 錯誤描述中不包含 %w時,p.wrappedErrnil,所以底層也是呼叫errors.New()建立錯誤。因此錯誤型別就是*errors.errorString

  2. 錯誤描述中包含%w時,p.wrappedErr不為nil,所以底層例項化wrapError結構體指標。 因此錯誤型別是*fmt.wrapError,可以理解為包裹錯誤型別。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章