Golang中error和建立error原始碼解析
Golang中的錯誤處理和Java,Python有很大不同,沒有try...catch
語句來處理錯誤。因此,Golang中的錯誤處理是一個比較有爭議的點,如何優雅正確的處理錯誤是值得去深究的。
今天先記錄error
是什麼及如何建立error
,擼一擼原始碼。
1.什麼是error
error
錯誤指的是可能出現問題的地方出現了問題。比如開啟一個檔案時失敗,這種情況在人們的意料之中 。
而異常指的是不應該出現問題的地方出現了問題。比如引用了空指標,這種情況在人們的意料之外。
可見,錯誤是業務過程的一部分,而異常不是 。
Golang中的錯誤也是一種型別。錯誤用內建的error
型別表示。就像其他型別,如int
,float64
等。
錯誤值可以儲存在變數中,也可以從函式中返回,等等。
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.wrappedErr
為 nil
的時候,會呼叫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
動詞時,將賦值為truewrappedErr
欄位,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()
來建立錯誤時,核心有以下兩點:
錯誤描述中不包含
%w
時,p.wrappedErr
為nil
,所以底層也是呼叫errors.New()
建立錯誤。因此錯誤型別就是*errors.errorString
。錯誤描述中包含
%w
時,p.wrappedErr
不為nil
,所以底層例項化wrapError
結構體指標。 因此錯誤型別是*fmt.wrapError
,可以理解為包裹錯誤型別。
本作品採用《CC 協議》,轉載必須註明作者和本文連結