Go 1.13發布的功能還有一個值得深入研究的,就是對Error的增強,也是今天我們要分析的 Error Wrapping.
背景
做Go語言開發的,肯定經常用error,但是我們也知道error非常弱,只能自帶一串文本其他什么都做不了,比如給已經存在的error增加一些附加文本,增加堆棧信息等都做不了。如果我們想給error增加一些附加文本怎么做呢?有兩種辦法:
第一種:
newErr:=fmt.Errorf("數據上傳問題: %v", err)
通過fmt.Errorf函數,基於已經存在的err再生成一個新的newErr,然后附加上我們想添加的文本信息。這種辦法比較方便,但是問題也很明顯,我們丟失了原來的err,因為它已經被我們的fmt.Errorf函數轉成一個新的字符串了。
第二種:
func main() {
newErr := MyError{err, "數據上傳問題"}
}
type MyError struct {
err error
msg string
}
func (e *MyError) Error() string {
return e.err.Error() + e.msg
}
這種方式就是我們自定義自己的struct,添加用於存儲我們需要額外信息的字段,我這里是err和msg,分別存儲原始err和新附加的出錯信息。然后這個MyError還會實現error接口,表示他是一個error,這樣我們就可以自由的使用了。
這種方式有點很明顯,大家可以看到了。缺點呢,就是我們要自定義很多struct,而且我們自己定義的,和第三方的可能還不太一樣,無法統一和兼容。基於這個背景,Golang 1.13 為我們提供了Error Wrapping,翻譯過來我更願意叫Error嵌套。
如何生成一個Wrapping Error
Error Wrapping,顧名思義,就是為我們提供了,可以一個error嵌套另一個error功能,好處就是我們可以根據嵌套的error序列,生成一個error錯誤跟蹤鏈,也可以理解為錯誤堆棧信息,這樣可以便於我們跟蹤調試,哪些錯誤引起了什么問題,根本的問題原因在哪里。
因為error可以嵌套,所以每次嵌套的時候,我們都可以提供新的錯誤信息,並且保留原來的error。現在我們看下如何生成一個嵌套的error。
e := errors.New("原始錯誤e")
w := fmt.Errorf("Wrap了一個錯誤%w", e)
Golang並沒有提供什么Wrap函數,而是擴展了fmt.Errorf函數,加了一個%w來生成一個可以Wrapping Error,通過這種方式,我們可以創建一個個以Wrapping Error。
Wrapping Error原理
按照這種不丟失原error的思路,那么Wrapping Error的實現原理應該類似我們上面的自定義error.我們看下fmt.Errorf函數的源代碼驗證下我們的猜測是否正確。
func Errorf(format string, a ...interface{}) error {
//省略無關代碼
var err error
if p.wrappedErr == nil {
err = errors.New(s)
} else {
err = &wrapError{s, p.wrappedErr}
}
p.free()
return err
}
這里的關鍵核心代碼就是p.wrappedErr的判斷,這個值是否存在,決定是否要生成一個wrapping error。這個值是怎么來的呢?就是根據我們設置的%w解析出來的。
有了這個值之后,就生成了一個&wrapError{s, p.wrappedErr}返回了,這里有個結構體wrapError
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
如上所示,和我們想的一樣。實現了Error方法說明它是一個error。Unwrap方法是一個特別的方法,所有的wrapping error 都會有這么一個方法,用於獲得被嵌套的error。
Unwrap 函數
Golang 1.13引入了wrapping error后,同時為errors包添加了3個工具函數,他們分別是Unwrap、Is和As,先來聊聊Unwrap。
顧名思義,它的功能就是為了獲得被嵌套的error。
func main() {
e := errors.New("原始錯誤e")
w := fmt.Errorf("Wrap了一個錯誤%w", e)
fmt.Println(errors.Unwrap(w))
}
以上這個例子,通過errors.Unwrap(w)后,返回的其實是個e,也就是被嵌套的那個error。
這里需要注意的是,嵌套可以有很多層,我們調用一次errors.Unwrap函數只能返回最外面的一層error,如果想獲取更里面的,需要調用多次errors.Unwrap函數。最終如果一個error不是warpping error,那么返回的是nil。
func Unwrap(err error) error {
//先判斷是否是wrapping error
u, ok := err.(interface {
Unwrap() error
})
//如果不是,返回nil
if !ok {
return nil
}
//否則則調用該error的Unwrap方法返回被嵌套的error
return u.Unwrap()
}
看看該函數的的源代碼吧,這樣就會理解的更深入一些,我加了一些注釋。
Is 函數
在Go 1.13之前沒有wrapping error的時候,我們要判斷error是不是同一個error可以使用如下辦法:
if err == os.ErrExist
這樣我們就可以通過判斷來做一些事情。但是現在有了wrapping error后這樣辦法就不完美的,因為你根本不知道返回的這個err是不是一個嵌套的error,嵌套了幾層。所以基於這種情況,Golang為我們提供了errors.Is函數。
func Is(err, target error) bool
-
如果
err和target是同一個,那么返回true -
如果
err是一個wrap error,target也包含在這個嵌套error鏈中的話,那么也返回true。
很簡單的一個函數,要么咱倆相等,要么err包含target,這兩種情況都返回true,其余返回false。
func Is(err, target error) bool {
if target == nil {
return err == target
}
isComparable := reflectlite.TypeOf(target).Comparable()
//for循環,把err一層層剝開,一個個比較,找到就返回true
for {
if isComparable && err == target {
return true
}
//這里意味着你可以自定義error的Is方法,實現自己的比較代碼
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
//剝開一層,返回被嵌套的err
if err = Unwrap(err); err == nil {
return false
}
}
}
Is函數源代碼如上,其實就是一層層反嵌套,剝開然后一個個的和target比較,相等就返回true。
As 函數
在Go 1.13之前沒有wrapping error的時候,我們要把error轉為另外一個error,一般都是使用type assertion 或者 type switch,其實也就是類型斷言。
if perr, ok := err.(*os.PathError); ok {
fmt.Println(perr.Path)
}
比如例子中的這種方式,但是現在給你返回的err可能是已經被嵌套了,甚至好幾層了,這種方式就不能用了,所以Golang為我們在errors包里提供了As函數,現在我們把上面的例子,用As函數實現一下。
var perr *os.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path)
}
這樣就可以了,就可以完全實現類型斷言的功能,而且還更強大,因為它可以處理wrapping error。
func As(err error, target interface{}) bool
從功能上來看,As所做的就是遍歷err嵌套鏈,從里面找到類型符合的error,然后把這個error賦予target,這樣我們就可以使用轉換后的target了,這里有值得賦予,所以target必須是一個指針。
func As(err error, target interface{}) bool {
//一些判斷,保證target,這里是不能為nil
if target == nil {
panic("errors: target cannot be nil")
}
val := reflectlite.ValueOf(target)
typ := val.Type()
//這里確保target必須是一個非nil指針
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
panic("errors: target must be a non-nil pointer")
}
//這里確保target是一個接口或者實現了error接口
if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
panic("errors: *target must be interface or implement error")
}
targetType := typ.Elem()
for err != nil {
//關鍵部分,反射判斷是否可被賦予,如果可以就賦值並且返回true
//本質上,就是類型斷言,這是反射的寫法
if reflectlite.TypeOf(err).AssignableTo(targetType) {
val.Elem().Set(reflectlite.ValueOf(err))
return true
}
//這里意味着你可以自定義error的As方法,實現自己的類型斷言代碼
if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
return true
}
//這里是遍歷error鏈的關鍵,不停的Unwrap,一層層的獲取err
err = Unwrap(err)
}
return false
}
這是As函數的源代碼,看源代碼比較清晰一些,我在代碼里做了注釋,這里就不一一分析了,大家可以結合注釋讀一下。
舊工程遷移
新特性的更新,如果要使想使用,不免會有舊項目的遷移,現在我們就針對幾種常見的情況看如何進行遷移。
如果你以前是直接返回err,或者通過如下方式給err增加了額外信息。
return err
return fmt.Errorf("more info: %v", err)
這2種情況你直接切換即可。
return fmt.Errorf("more info: %w", err)
切換后,如果你有==的error判斷,那么就用Is函數代替,比如:
舊工程
if err == os.ErrExist
新工程
if errors.Is(err, os.ErrExist)
同理,你舊的代碼中,如果有對error進行類型斷言的轉換,就要用As函數代替,比如:
舊工程
if perr, ok := err.(*os.PathError); ok {
fmt.Println(perr.Path)
}
新工程
var perr *os.PathError
if errors.As(err, &perr) {
fmt.Println(perr.Path)
}
如果你自己自定義了一個struct實現了error接口,而且還嵌套了error,這個時候該怎么適配新特性呢?也就是我們上面舉例的情況:
type MyError struct {
err error
msg string
}
func (e *MyError) Error() string {
return e.err.Error() + e.msg
}
其實對於這種方式很簡單,只需要給他添加一個Unwrap方法就可以了,讓它變成一個wrap error。
func (e *MyError) Unwrap() error {
return e.err
}
這樣就可以了。
結語
這篇文章深度剖析了Error Wrapping的背景、實現原理以及工程遷移的注意事項,這個新特性是很值得使用的,畢竟增強了error,提供了強大的跟蹤能力,再結合一些額外的數據,可以讓我們調試、變成會更方便。
原文:https://www.flysnow.org/2019/09/06/go1.13-error-wrapping.html
