Go Errors 詳解

WilburXu 發表於 2022-01-17
Go

原文地址:Go Errors詳解

Golang 中的錯誤處理和 PHP、JAVA 有很大不同,沒有 try...catch 語句來處理錯誤。因此,Golang 中的錯誤處理是一個比較有爭議的點,如何更好的 理解處理 錯誤資訊是值得去深入研究的。

Go 內建 errors

Go error 是一個介面型別,它包含一個 Error() 方法,返回值為 string。任何實現這個介面的型別都可以作為一個錯誤使用,Error 這個方法提供了對錯誤的描述:

// http://golang.org/pkg/builtin/#error
// error 介面的定義
type error interface {
    Error() string
}

// http://golang.org/pkg/errors/error.go
// errors 構建 error 物件
type errorString struct {
    s string
}

func (e *errorString) Error() string {
    return e.s
}

error 是一個介面型別,它包含一個 Error() 方法,返回值為 string。只要實現這個 interface 都可以作為一個錯誤使用,Error 這個方法提供了對錯誤的描述。

error 建立

error 的建立方式有兩種方法:

1. errors.New()

// New returns an error that formats as the given text.
// Each call to New returns a distinct error value even if the text is identical.
func New(text string) error {
    return &errorString{text}
}

Q:為何 errors.New() 要返回指標?

A:避免 New 的內容相當,造成的歧義,看看下面的例子就可以理解為什麼了:

func main() {
    errOne := errors.New("test")
    errTwo := errors.New("test")

    if errOne == errTwo {
      log.Printf("Equal \n")
    } else {
      log.Printf("notEqual \n")
    }
}

輸出:

notEqual

如果使用 errorString 的值去比較,當專案逐漸盤大、複雜,對於 New() 內容也就難以保證唯一,到那時對於問題的排查,也將是災難性的。

有些時候我們需要更加具體的資訊。即需要具體的 “上下文” 資訊,表明具體的錯誤值。

這就用到了 fmt.Errorf 函式

2. fmt.Errorf()

fmtErr := fmt.Errorf("fmt.Errorf() err, http status is %d", 404)
fmt.Printf("fmtErr errType:%T,err: %v\n", fmtErr, fmtErr)

輸出:

fmtErr errType is *errors.errorString,err is fmt.Errorf() err, http status is 404

為什麼 fmtErr 返回的錯誤型別也是 :*errors.errorString,我們不是用 fmt.Errorf() 建立的嗎?

一起來看下原始碼:

// Errorf formats according to a format specifier and returns the string as a
// value that satisfies error.
//
// If the format specifier includes a %w verb with an error operand,
// the returned error will implement an Unwrap method returning the operand. It is
// invalid to include more than one %w verb or to supply it with an operand
// that does not implement the error interface. The %w verb is otherwise
// a synonym for %v.
func Errorf(format string, a ...interface{}) 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.wrappedErrnil 的時候,會呼叫 errors.New() 來建立錯誤。

那問題來了,這個 p.wrappedErr 是什麼?

我們來看個例子:

wErrOne := errors.New("this is one ")
wErrTwo := fmt.Errorf("this is two %w", wErrOne)
fmt.Printf("wErrOne type is %T err is %v \n", wErrOne, wErrOne)
fmt.Printf("wErrTwo type is %T err is %v \n", wErrTwo, wErrTwo)

輸出:

wErrOne type is *errors.errorString err is this is one  
wErrTwo type is *fmt.wrapError err is this is two this is one  

發現沒有?使用 %w 返回的 error 物件,輸出的型別是 *fmt.wrapError

%w 是 go 1.13 新增加的錯誤處理特性 。

Go 錯誤處理實踐

如何獲得更詳細錯誤資訊,比如stack trace,幫助定位錯誤原因?

有人說,層層打 log,但這會造成日誌打得到處都是,難以維護。

又有人說,使用 recover 捕獲 panic,但是這樣會導致 panic 的濫用。

panic 只用於真正異常的情況,如

  • 在程式啟動的時候,如果有強依賴的服務出現故障時 panic 退出
  • 在程式啟動的時候,如果發現有配置明顯不符合要求, 可以 panic 退出(防禦程式設計)
  • 在程式入口處,例如 gin 中介軟體需要使用 recovery 預防 panic 程式退出

pkg/errors 庫

這裡,我們通過一個很小的包 github.com/pkg/errors 來試圖解決上面的問題。

看一個案例:

package main

import (
    "github.com/pkg/errors"
    "log"
    "os"
)

func main() {
    err := mid()
    if err != nil {
      // 返回 err 的根本原因
      log.Printf("cause is %+v \n", errors.Cause(err))

      // 返回 err 呼叫的堆疊資訊
      log.Printf("strace tt %+v \n", err)
    }
}

func mid() (err error) {
        return test()
}


func test() (err error) {
        _, err = os.Open("test/test.txt")
    if err != nil {
          return errors.Wrap(err, "open error")
    }

    return nil
}

輸出:

2022/01/17 00:26:17 cause is open test.test: no such file or directory 
2022/01/17 00:26:17 strace tt open test.test: no such file or directory
open error
main.test
        /path/err/wrap_t/main.go:41
main.mid
        /path/err/wrap_t/main.go:35
main.main
        /path/err/wrap_t/main.go:13
runtime.main
        /usr/local/Cellar/go/1.17.2/libexec/src/runtime/proc.go:255
runtime.goexit
        /usr/local/Cellar/go/1.17.2/libexec/src/runtime/asm_amd64.s:1581 

pkg/errors

上層呼叫者使用errors.Cause(err)方法就能拿到這次錯誤造成的罪魁禍首。

// Wrap returns an error annotating err with a stack trace
// at the point Wrap is called, and the supplied message.
// If err is nil, Wrap returns nil.
func Wrap(err error, message string) error {
   if err == nil {
      return nil
   }
   err = &withMessage{
      cause: err,
      msg:   message,
   }
   return &withStack{
      err,
      callers(),
   }
}
// Is 指出當前的錯誤鏈是否存在目標錯誤。
func Is(err, target error) bool

// As 檢查當前錯誤鏈上是否存在目標型別。若存在則ok為true,e為型別轉換後的結果。若不存在則ok為false,e為空值
func As(type E)(err error) (e E, ok bool)

參考

https://go.googlesource.com/p...

https://github.com/golang/go/...

https://github.com/pkg/errors

https://go.googlesource.com/p...

https://go.googlesource.com/p...