Go語言(golang)新發布的1.13中的Error Wrapping深度分析


 

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,添加用於存儲我們需要額外信息的字段,我這里是errmsg,分別存儲原始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方法說明它是一個errorUnwrap方法是一個特別的方法,所有的wrapping error 都會有這么一個方法,用於獲得被嵌套的error。

Unwrap 函數

Golang 1.13引入了wrapping error后,同時為errors包添加了3個工具函數,他們分別是UnwrapIsAs,先來聊聊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
  1. 如果errtarget是同一個,那么返回true

  2. 如果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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM