前言
快一個月沒有更新技術文章了,這段時間投注了較多的時間學習位元組的開源項目 Kitex/Hertz ,并維護一些簡單的 issue ,有興趣的同學也可以去了解:
www.cloudwego.io/
這段時間遲遲沒有更新文章,一方面是接觸到了很多大佬,反觀自身技術深度遠遠不及,變得不敢輕易下筆;另一方面反思了一下自己之前的寫作,确實也有一些功利的成分,有時為了更新而更新,打算糾正。
接觸開源之後,我感受到了開源社群打磨一個項目的認真與嚴謹,後續也希望自己能以此為鑒,對開源、對寫作都是如此。
扯遠了,寫作這篇文章的原因是我在寫單元測試的時候,有時會涉及
errors.Is
和
errors.As
方法的調用,借此做一個總結。
error 的定義
首先需要明确 Go 語言中的錯誤是通過接口定義的,是以是一個引用類型。
type error interface {
Error() string
}
// Go 提供了一個預設實作
type errorString struct {
s string
}
func (e *errorString) Error() string {
return
那麼如果我要建立一個 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
通過
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
核心就是
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
如果這個 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 接口。