Go 1.13 之後的 error 檢查
起步
如果說 Go 有很多詬病的地方,那麼 Go 中 error 的處理一定可以擠進吐槽榜單前十。既然 try
語句提議被一拒再拒,我們也只好用着古老的 if 篩選錯誤。Go 官方並非沒有意識到 error 的雞肋問題,於是在 Go 1.13 提出了新解決方案,總的說來就是“三個 api + 一個格式佔位符”。
error 從何而來
在 Go 中,error 從何而來呢?熟練使用 Go 的人一定知道以下幾種方式:
- errors.New
- fmt.Errorf
- 直接返回函數調用後得到的 error
- 定義一個結構體,實現 error 接口(
type error Interface { Error() string }
)
以“打開一個文件”爲例,按照上面的處理方式代碼可以分別是:
// errors.New
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 返回新的 error 實例
return errors.New("can not open the specific file")
}
return nil
}
// fmt.Errorf
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 返回新的 error 實例,同時包含了實際 error
return fmt.Errorf("can not open the specific file, reason: %v", err)
}
return nil
}
// return error that called function returned
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
// 直接返回得到的 error,不做任何處理
return err
}
return nil
}
// 自定義 error
type OpenErr struct {
Err error // 存放原始 error
}
// 實現 error 接口
func (*OpenErr) Error() string {
return "can not open the specific file"
}
func openConfigFile(path string) error {
_, err := os.Open(path)
if err != nil {
return &OpenErr{Err:err}
}
return nil
}
上述四種方法各有千秋。如方法一,errors.New 會返回一個新的 error,其存放的數據就是我們傳入的 text(“can not open the specific file”)。採用這種方式通常是爲了告訴調用者出錯了,但實際的錯誤細節不願暴露。對調用者來說,他可能不太關心出了什麼錯,只在乎有沒有出錯。
方法二跟方法一相同,也會隱藏原始錯誤(這裏指錯誤類型),但通常會將原始錯誤的字符串說明一起返回。在該處理方式中,“can not open the specific file” 爲額外提示語,“reason: %v” 顯示錯誤細節。一般來講,調用 fmt.Errorf 更大機率是要產生一個給人看的錯誤,而不是讓代碼去解析(儘管並非不能)。
方法三,通常是函數調用者需要根據 error 的實際類型,或實際值,確定下一步的執行策略。也就是說它需要解析 error,所以函數返回“原味” error。這樣做的缺點是,沒辦法對 error 添加額外信息。
方法四可以認爲是上述三種方法的集合,既可以保留原始 error,還可以添加額外信息。通過這些元數據隨意組合,調用者想要的樣子它都有。缺點就是代碼要多寫一些。
如何檢查 error
現在 error 有了,我們應該如何檢查錯誤呢?在 1.13 之前,常見有:1. 比較值;2. 比較類型。
拿官方源碼舉例更具說服力。我們先來看看比較值。
func (db *DB) QueryContext(ctx context.Context,
query string,
args ...interface{}) (*Rows, error) {
var rows *Rows
var err error
for i := 0; i < maxBadConnRetries; i++ {
rows, err = db.query(ctx, query, args, cachedOrNewConn)
if err != driver.ErrBadConn { // 比較值
break
}
}
if err == driver.ErrBadConn { // 比較值
return db.query(ctx, query, args, alwaysNewConn)
}
return rows, err
}
func (db *DB) Query(query string,
args ...interface{}) (*Rows, error) {
return db.QueryContext(context.Background(), query, args...)
}
var ErrBadConn = errors.New("driver: bad connection")
上述是 sql.Query
的源碼,確定處理 ErrBadConn 錯誤,就是通過比較值實現的。
比較類型處理 error 在 Go 源碼中更常用,主要是這種處理方式更靈活,錯誤信息更豐富。
我們先寫一個 demo,代碼的主要目的是訪問某網站。訪問過程中可能出現各種異常,而我們只處理超時異常。
// http 客戶端
client := &http.Client{
Timeout: 3 * time.Second, // 3 秒超時
}
// 請求不存在的 url
_, err := client.Get("http://www.meiyuouzhegewangzhan.com")
if err != nil {
if os.IsTimeout(err) { // 超時 err
fmt.Println("timeout")
} else { // 其他 err
fmt.Println("other errors")
}
}
從上得知,判斷一個 error 是否是超時異常,需要調用 os.IsTimeout
,源碼如下。
func IsTimeout(err error) bool {
// 類型斷言
terr, ok := underlyingError(err).(timeout)
return ok && terr.Timeout()
}
func underlyingError(err error) error {
// 返回實際 err
switch err := err.(type) {
case *PathError:
return err.Err
case *LinkError:
return err.Err
case *SyscallError:
return err.Err
}
// 如果沒有潛在 err, 返回自身
return err
}
不論是 IsTimeout
還是 underlyingError
都藉助類型比較實現對不同錯誤類型做處理。另外,繼續跟進 PathError、LinkError、SysCallError 你會發現,這類 error 都是之前提到的第四種方法。它們包裝了原始 error,但又會在一定場合下把它取出來(如上述代碼中的 err.Err
)。
1.13+ 如何檢查 error
不得不承認,過去檢查 error 的方式有點麻煩,尤其是在 error 被包裝過多時。假設一個 error 被包裝了 3 層,此時又需要檢查最裏層的 error,意味着代碼需要這樣寫:
e1, _ := e.(Err1)
e2, _ := e1.(Err2)
e3, _ := e2.(Err3)
if e3 == targetErr {
// handle
}
爲了避免這種麻煩,1.13 開始,Go 提供了兩個方法,用於檢查鏈式中的 error。分別是比較值的 errors.Is
,以及比較類型的 errors.As
。想支持 error 鏈的檢查,要求結構體(或其他錯誤類型)實現匿名接口:interface { Unwrap() error }
。
Unwrap
Unwrap 接口很好理解,用於返回結構體中的原始 error。拿 PathError 的源碼舉例:
type PathError struct {
Op string
Path string
Err error
}
func (e *PathError) Unwrap() error { return e.Err }
errors.Is
errors.Is 通過值比較確定錯誤,這裏有易錯點。由於許多 error 是指針類型,而指針類型的比較是:比較指針變量存放的地址是否相同。用代碼解釋更容易理解:
err1 := errors.New("error")
err2 := errors.New("error")
// 指針比較
fmt.Println(err1 == err2) // false
// 值比較
err1Elem := reflect.ValueOf(err1).Elem().Interface()
err2Elem := reflect.ValueOf(err2).Elem().Interface()
fmt.Println(err1Elem == err2Elem) // true
err1 與 err2 本質上可以認爲是一個 error,連錯誤信息都相同,但直接比較會得到 false。這是因爲 err1 與 err2 有不同的地址。第二種方式則是將兩個指針指向的結構體取出來,比較兩個結構體值,從而獲得 true 的結果。
官方包對這類問題的處理常常是,定義一組錯誤變量,在之後的整個程序運行期間都使用已經賦值的錯誤變量——確保同一類錯誤的指針總是指向同一個地址。
// go 源碼
var (
// ErrInvalid indicates an invalid argument.
// Methods on File will return this error when the receiver is nil.
ErrInvalid = errInvalid() // "invalid argument"
ErrPermission = errPermission() // "permission denied"
ErrExist = errExist() // "file already exists"
ErrNotExist = errNotExist() // "file does not exist"
ErrClosed = errClosed() // "file already closed"
ErrNoDeadline = errNoDeadline() // "file type does not support deadline"
)
func errInvalid() error { return oserror.ErrInvalid }
func errPermission() error { return oserror.ErrPermission }
func errExist() error { return oserror.ErrExist }
func errNotExist() error { return oserror.ErrNotExist }
func errClosed() error { return oserror.ErrClosed }
func errNoDeadline() error { return poll.ErrNoDeadline }
判斷文件是否存在的兩種方式:
var err error
f, err := os.Open("不存在文件")
defer f.Close()
// 方法1
if os.IsNotExist(err) { // 進入 if stmt
fmt.Println("文件不存在")
}
// 方法2
if errors.Is(err, os.ErrNotExist) { // 進入 if stmt
fmt.Println("文件不存在")
}
所以根據以上特性,errors.Is 幾乎不能處理 error 鏈。聰明的官方庫怎麼會想不到這點,解決方案要從源碼找:
// go 源碼
func Is(err, target error) bool {
...
isComparable := reflectlite.TypeOf(target).Comparable()
for {
// 值比較
if isComparable && err == target {
return true
}
// 調用 Is 比較
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
...
if err = Unwrap(err); err == nil {
return false
}
}
}
errors.Is 會先進行值比較,如果失敗,那就有 Is 調用 Is,所以我們可以給自定義的 error 實現 Is 方法,用於填寫 error 是否相等的處理邏輯。
// Err1
type Err1 struct {
Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
func (e * Err1) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指針直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指針指向的變量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// Err2
type Err2 struct {
Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
func (e *Err2) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指針直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指針指向的變量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// Err3
type Err3 struct {
Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
func (e *Err3) Is(other error) bool {
v1 := reflect.ValueOf(e)
v2 := reflect.ValueOf(other)
// 如果是空指針直接返回
if v1.IsNil() || v2.IsNil() {
return false
}
// 取出指針指向的變量
v1 = v1.Elem()
if v2.Kind() == reflect.Ptr {
v2 = v2.Elem()
}
return v1.IsValid() && v2.IsValid() && v1.Interface() == v2.Interface()
}
// 產生 error
func genErr() error {
return &Err1{
Err: &Err2{
Err: &Err3{
Err: nil,
},
},
}
}
func main() {
err := genErr()
err3 := &Err3{Err:nil}
fmt.Println(errors.Is(err, err3))
}
代碼明顯累贅起來,往下看,還有更優雅的辦法。
errors.As
當存在錯誤鏈時,我們更傾向於用類型定位錯誤,使用 errors.As 而不是 errors.Is。這樣做也能避免冗餘的 Is 方法。
// Err1
type Err1 struct {
Err error
}
func (e *Err1) Error() string { return "err1" }
func (e *Err1) Unwrap() error { return e.Err }
// Err2
type Err2 struct {
Err error
}
func (e *Err2) Error() string { return "err2" }
func (e *Err2) Unwrap() error { return e.Err }
// Err3
type Err3 struct {
Err error
}
func (e *Err3) Error() string { return "err3" }
func (e *Err3) Unwrap() error { return e.Err }
// 產生 error
func genErr() error {
return &Err1{
Err: &Err2{
Err: &Err3{
Err: nil,
},
},
}
}
func main() {
err := genErr()
var err3 *Err3
fmt.Println(errors.As(err, &err3)) // 第二個參數要求是 指針的地址
}
佔位符 %w
大多數情況下我們用不着大動干戈的自定義錯誤類型,而喜歡採用第二種方式 fmt.Errorf
。從 1.13 開始,使用 %w 佔位符會把原始 error 包裹起來放到 err 字段裏,並構建一個新的 error 字符串放到 msg 中,對應的結構體就是 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 鏈,方便 errors.Is 與 errors.As 進行錯誤檢查。
if _, err := os.Open("xxx.txt") {
return fmt.Errorf("failed open, reason: %w", err)
}