快一個月沒有更新技術文章了,這段時間投注了較多的時間學習位元組的開源專案 Kitex/Hertz ,並維護一些簡單的 issue ,有興趣的同學也可以去了解:
這段時間遲遲沒有更新文章,一方面是接觸到了很多大佬,反觀自身技術深度遠遠不及,變得不敢輕易下筆;另一方面反思了一下自己之前的寫作,確實也有一些功利的成分,有時為了更新而更新,打算糾正。
接觸開源之後,我感受到了開源社群打磨一個專案的認真與嚴謹,後續也希望自己能以此為鑑,對開源、對寫作都是如此。
扯遠了,寫作這篇文章的原因是我在寫單元測試的時候,有時會涉及 errors.Is
和 errors.As
方法的呼叫,藉此做一個總結。
首先需要明確 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 是巢狀的 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)
的實現印證這個流程。
判斷被包裝的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 相等。
提取指定型別的錯誤,判斷包裝的 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 倉庫,正在更新中,你也可以從我往期的文章中看到一些說明。