Go 源碼解讀|如何用好 errors 庫的 errors.Is() 與 errors.As() 方法
前言
快一個月沒有更新技術文章了,這段時間投注了較多的時間學習位元組的開源項目 Kitex/Hertz ,並維護一些簡單的 issue ,有興趣的同學也可以去了解:
這段時間遲遲沒有更新文章,一方面是接觸到了很多大佬,反觀自身技術深度遠遠不及,變得不敢輕易下筆;另一方面反思了一下自己之前的寫作,確實也有一些功利的成分,有時為了更新而更新,打算糾正。
接觸開源之後,我感受到了開源社區打磨一個項目的認真與嚴謹,後續也希望自己能以此為鑒,對開源、對寫作都是如此。
扯遠了,寫作這篇文章的原因是我在寫單元測試的時候,有時會涉及 errors.Is
和 errors.As
方法的調用,藉此做一個總結。
error 的定義
首先需要明確 Go 語言中的錯誤是通過介面定義的,因此是一個引用類型。
type error interface {
Error() string
}
// Go 提供了一個默認實現
type errorString struct {
s string
}
func (e *errorString) Error() string {
return e.s
}
那麼如果我要創建一個 error 實例,可以選擇下面的方式:
func main() {
// 此時創建的兩個 error 都是 errorString 結構類型的
errA := errors.New("new error a")
fmt.Println(errA)
errB := fmt.Errorf("new error %s", "b")
fmt.Println(errB)
}
/*
列印結果:
new error a
new error b
*/
wrapError 的定義
wrapError 是嵌套的 error ,也實現了 error 介面的 Error
方法,本質也是一個 error ,並聲明了一個 Unwrap
方法用於拆包裝。
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
通過 fmt.Errorf
方法配合 %w
佔位符創建嵌套類型的 wrapError。
var BaseErr = errors.New("the underlying base error")
func main() {
err1 := fmt.Errorf("wrap base: %w", BaseErr)
fmt.Println(err1)
err2 := fmt.Errorf("wrap err1: %w", err1)
fmt.Println(err2)
}
/*
列印結果:
wrap base: the underlying base error
wrap err1: wrap base: the underlying base error
*/
為什麼 fmt.Errorf
用了佔位符 %w
之後創建的就是 wrapError 類型,而用了 fmt.Errorf
但只是選擇其他佔位符如上述示例中的 %s
創建的就是 errorString 類型?
可以簡單看一下 fmt.Errorf
方法的源碼:
func Errorf(format string, a ...any) 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.doPrintf(format, a)
調用後,如果包含 %w
佔位符則會先創建內層的 error ,賦值給 p.wrappedErr
,從而觸發 wrapError 的創建邏輯。
你也可以進一步去看 p.doPrintf(format, a)
的實現印證這個流程。
errors.Is
判斷被包裝的error是否包含指定錯誤。
var BaseErr = errors.New("the underlying base error")
func main() {
err1 := fmt.Errorf("wrap base: %w", BaseErr)
err2 := fmt.Errorf("wrap err1: %w", err1)
println(err2 == BaseErr) // false
if !errors.Is(err2, BaseErr) {
panic("err2 is not BaseErr")
}
println("err2 is BaseErr")
}
/*
列印結果:
false
err2 is BaseErr
*/
來看一下 errors.Is
方法的源碼:
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
for {
if isComparable && err == target {
return true
}
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
if err = Unwrap(err); err == nil {
return false
}
}
}
func Unwrap(err error) error {
u, ok := err.(interface {
Unwrap() error
})
if !ok {
return nil
}
return u.Unwrap()
}
如果這個 err 自己實現了 interface{ Is(error) bool }
介面,通過介面斷言,可以調用 Is
方法判斷 err 是否與 target 相等。
否則遞歸調用 Unwrap
方法拆包裝,返回下一層的 error 去判斷是否與 target 相等。
errors.As
提取指定類型的錯誤,判斷包裝的 error 鏈中,某一個 error 的類型是否與 target 相同,並提取第一個符合目標類型的錯誤的值,將其賦值給 target。
type TypicalErr struct {
e string
}
func (t TypicalErr) Error() string {
return t.e
}
func main() {
err := TypicalErr{"typical error"}
err1 := fmt.Errorf("wrap err: %w", err)
err2 := fmt.Errorf("wrap err1: %w", err1)
var e TypicalErr
if !errors.As(err2, &e) {
panic("TypicalErr is not on the chain of err2")
}
println("TypicalErr is on the chain of err2")
println(err == e)
}
/*
列印結果:
TypicalErr is on the chain of err2
true
*/
來看一下 error.As
方法的源碼:
func As(err error, target any) bool {
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
targetType := typ.Elem()
if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
for err != nil {
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
return true
}
err = Unwrap(err)
}
return false
}
源碼 for 循環前的部分是用來約束 target 參數的類型,要求其是一個非空的指針類型。
此外要求 *target
是一個介面或者實現了 error 介面。
for 循環判斷 err 是否可以賦值給 target 所屬類型,如果可以則賦值返回 true。
如果 err 實現了自己的 As
方法,則調用其邏輯,否則也是走遞歸拆包的邏輯。
小結
後續將繼續分享一些源碼解讀的文章,關於 Go 語言的學習,我也開源了一個 GitHub 倉庫,正在更新中,你也可以從我往期的文章中看到一些說明。