1. 程式人生 > 其它 >golang error錯誤處理

golang error錯誤處理

error定義

資料結構

go語言error是一普通的值,實現方式為簡單一個介面。

// The error built-in interface type is the conventional interface for
// representing an error condition, with the nil value representing no error.

type error interface {
    Error() string
}

建立error

使用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}
}

// errorString is a trivial implementation of error.
type errorString struct {
    s string
}

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

返回的是errorString結構體 實現了error介面的Error()方法

使用fmt.Errorf()建立

建立方式為把字串拼接起來,然後呼叫errors.New().

基礎庫中的自定義的error

bufio中的錯誤:

ErrTooLong         = errors.New("bufio.Scanner: token too long")
ErrNegativeAdvance = errors.New("bufio.Scanner: SplitFunc returns negative advance count")
ErrAdvanceTooFar   = errors.New("bufio.Scanner: SplitFunc returns advance count beyond input")
ErrBadReadCount    = errors.New("bufio.Scanner: Read returned impossible count")

error的比較

package main

import (
    "errors"
    "fmt"
)

type errorString struct {
    s string
}

func new(s string) error {
    return &errorString{s: s}
}

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

func main() {
    error1 := errors.New("test")
    error2 := new("test")
    fmt.Println(error1 == error2) // false
}

// 比較結構體
package main

import (
    "fmt"
)

type errorString struct {
    s string
}

func new(s string) error {
    return &errorString{s: s}
}

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

func main() {
    error1 := new("test")
    fmt.Println(error1 == new("test")) // false
}
package main

import (
    "fmt"
)

type errorString struct {
    s string
}

func new(s string) error {
    return errorString{s: s}
}

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

func main() {
    error1 := new("test")
    fmt.Println(error1 == new("test")) // true
}

error對比為對比對比實現interface的結構體型別 和結構體本身

Error or Exception

處理錯誤的演進

  1. C
  • 單返回值,一般通過傳遞指標作為入參,返回值為 int 表示成功還是失敗。
  1. C++
  • 引入了 exception,但是無法知道被呼叫方會丟擲什麼異常。
  1. Java
  • 引入了 checked exception,方法的所有者必須申明,呼叫者必須處理。在啟動時丟擲大量的異常是司空見慣的事情,並在它們的呼叫堆疊中盡職地記錄下來。Java 異常不再是異常,而是變得司空見慣了。它們從良性到災難性都有使用,異常的嚴重性由函式的呼叫者來區分
  1. go
  • Go 的處理異常邏輯是不引入 exception,支援多引數返回,所以你很容易的在函式簽名中帶上實現了 error interface 的物件,交由呼叫者來判定。

  • 如果一個函式返回了 value, error,你不能對這個 value 做任何假設,必須先判定 error。唯一可以忽略 error 的是,如果你連 value 也不關心。

  • Go 中有 panic 的機制,如果你認為和其他語言的 exception 一樣,那你就錯了。當我們丟擲異常的時候,相當於你把 exception 扔給了呼叫者來處理。比如,你在 C++ 中,把 string 轉為 int,如果轉換失敗,會丟擲異常。或者在 java 中轉換 string 為 date 失敗時,會丟擲異常。

  • Go panic 意味著 fatal error(就是掛了)。不能假設呼叫者來解決 panic,意味著程式碼不能繼續執行。

  • 使用多個返回值和一個簡單的約定,Go 解決了讓程式設計師知道什麼時候出了問題,併為真正的異常情況保留了 panic。

程式碼對比

package main

import "fmt"

func Positive(x int) bool {
    return x >= 0
}

func Check(x int) {
    if Positive(x) {
        fmt.Println("正數")
    } else {
        fmt.Println("負數")
    }
}

func main() {
    Check(-1) // 負數
    Check(0)  // 正數 bug
    Check(1)  // 正數
}
package main

import "fmt"

func Positive(x int) (bool, bool) {
    if x == 0 {
        return false, false
    }
    return x >= 0, true
}

func Check(x int) {
    t, ok := Positive(x)
    if !ok {
        fmt.Println("零")
        return
    }

    if t {
        fmt.Println("正數")
    } else {
        fmt.Println("負數")
    }

}

func main() {
    Check(-1) // 負數
    Check(0)  // 零
    Check(1)  // 正數
}
package main

import (
    "errors"
    "fmt"
)

func Positive(x int) (bool, error) {
    if x == 0 {
        return false, errors.New("為零")
    }
    return x >= 0, nil
}

func Check(x int) {
    t, err := Positive(x)
    if err != nil {
        fmt.Println(err)
        return
    }

    if t {
        fmt.Println("正數")
    } else {
        fmt.Println("負數")
    }

}

func main() {
    Check(-1) // 負數
    Check(0)  // 為零
    Check(1)  // 正數
}

error使用

對於真正意外的情況,那些表示不可恢復的程式錯誤,例如索引越界、不可恢復的環境問題、棧溢位,我們才使用 panic。對於其他的錯誤情況,我們應該是期望使用 error 來進行判定。

  • 簡單。

  • 考慮失敗,而不是成功。

  • 沒有隱藏的控制流。

  • 完全交給你來控制 error。

  • Error are values。

Sentinel Error

預定義的特定錯誤,我們叫為 sentinel error,這個名字來源於計算機程式設計中使用一個特定值來表示不可能進行進一步處理的做法。所以對於 Go,我們使用特定的值來表示錯誤。

if err == ErrSomething { … }

類似的 io.EOF,更底層的 syscall.ENOENT

使用 sentinel 值是最不靈活的錯誤處理策略,因為呼叫方必須使用 == 將結果與預先宣告的值進行比較。當您想要提供更多的上下文時,這就出現了一個問題,因為返回一個不同的錯誤將破壞相等性檢查。

甚至是一些有意義的 fmt.Errorf 攜帶一些上下文,也會破壞呼叫者的 == ,呼叫者將被迫檢視 error.Error() 方法的輸出,以檢視它是否與特定的字串匹配。

  • 不依賴檢查 error.Error 的輸出。

不應該依賴檢測 error.Error 的輸出,Error 方法存在於 error 介面主要用於方便程式設計師使用,但不是程式(編寫測試可能會依賴這個返回)。這個輸出的字串用於記錄日誌、輸出到 stdout 等。

  • Sentinel errors 成為你 API 公共部分。

如果您的公共函式或方法返回一個特定值的錯誤,那麼該值必須是公共的,當然要有文件記錄,這會增加 API 的表面積。

如果 API 定義了一個返回特定錯誤的 interface,則該介面的所有實現都將被限制為僅返回該錯誤,即使它們可以提供更具描述性的錯誤。

比如 io.Reader。像 io.Copy 這類函式需要 reader 的實現者比如返回 io.EOF 來告訴呼叫者沒有更多資料了,但這又不是錯誤。

  • Sentinel errors 在兩個包之間建立了依賴。

sentinel errors 最糟糕的問題是它們在兩個包之間建立了原始碼依賴關係。例如,檢查錯誤是否等於 io.EOF,您的程式碼必須匯入 io 包。這個特定的例子聽起來並不那麼糟糕,因為它非常常見,但是想象一下,當專案中的許多包匯出錯誤值時,存在耦合,專案中的其他包必須匯入這些錯誤值才能檢查特定的錯誤條件(in the form of an import loop)。

  • 結論: 儘可能避免 sentinel errors。

我的建議是避免在編寫的程式碼中使用 sentinel errors。在標準庫中有一些使用它們的情況,但這不是一個您應該模仿的模式。

錯誤型別

Error type 是實現了 error 介面的自定義型別。例如 MyError 型別記錄了檔案和行號以展示發生了什麼。

type Myerror struct {
    line int
    file string
    s    string
}

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

func new(file string, line int, s string) error {
    return &Myerror{line: line, file: file, s: s}
}

因為 MyError 是一個 type,呼叫者可以使用斷言轉換成這個型別,來獲取更多的上下文資訊。

err := new("main.go", 23, "test error")
switch err := err.(type) {
case nil:
    fmt.Println("err is nil")
case *Myerror:
    fmt.Println("type is *Myerror err line :", err.line)
default:
    fmt.Println("None of them")
}

// 結果:type is *Myerror err line : 23

與錯誤值相比,錯誤型別的一大改進是它們能夠包裝底層錯誤以提供更多上下文。

一個不錯的例子就是 os.PathError 他提供了底層執行了什麼操作、那個路徑出了什麼問題。

呼叫者要使用型別斷言和型別 switch,就要讓自定義的 error 變為 public。這種模型會導致和呼叫者產生強耦合,從而導致 API 變得脆弱。

結論是儘量避免使用 error types,雖然錯誤型別比 sentinel errors 更好,因為它們可以捕獲關於出錯的更多上下文,但是 error types 共享 error values 許多相同的問題。

因此,我的建議是避免錯誤型別,或者至少避免將它們作為公共 API 的一部分。

非透明的error

在我看來,這是最靈活的錯誤處理策略,因為它要求程式碼和呼叫者之間的耦合最少。

我將這種風格稱為不透明錯誤處理,因為雖然您知道發生了錯誤,但您沒有能力看到錯誤的內部。作為呼叫者,關於操作的結果,您所知道的就是它起作用了,或者沒有起作用(成功還是失敗)。

這就是不透明錯誤處理的全部功能–只需返回錯誤而不假設其內容

package main

import "os"

func test() error {
    f, err := os.Open("filename.txt")
    if err != nil {
        return err
    }
    // use f 
}

為行為而不是型別斷言錯誤 在少數情況下,這種二分錯誤處理方法是不夠的。例如,與程序外的世界進行互動(如網路活動),需要呼叫方調查錯誤的性質,以確定重試該操作是否合理。在這種情況下,我們可以斷言錯誤實現了特定的行為,而不是斷言錯誤是特定的型別或值。考慮這個例子:

// 封裝內部
type temporary interface {
    Temporary() bool
}

func IsTemporary(err error) bool {
    te, ok := err.(temporary)
    return ok && te.Temporary()
}

// net包的error
type Error interface {
    error
    Timeout() bool   // Is the error a timeout?
    Temporary() bool // Is the error temporary?
}

// 錯誤處理
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
    // 處理
    return
}

if err != nil {

}

Handling Error

無錯誤的正常流程程式碼,將成為一條直線,而不是縮排的程式碼。

f,err  := os.Open("file")
if err != nil {
    // 處理錯誤
    return
}

// 邏輯
f,err  = os.Open("file2")
if err != nil {
    // 處理錯誤
    return
}

// 邏輯

通過消除錯誤消除錯誤處理

// 改進前
func AutoRusquest() (err error) {
  err = Anto()
  if err != nil {
    return
  }  
  return
}

// 改進後
func AutoRusquest() (err error) {
    return Anto()
}

func main() {
    err := AutoRusquest()
    if err != nil {
        // log 
    }
}

// 統計行數
func Countlines(r io.Reader) (int, error) {
    var (
        br    = bufio.NewReader(r)
        lines int
        err   error
    )

    for {
        _, err = br.ReadString('\n')
        lines++
        if err != nil {
            break
        }
    }

    if err != io.EOF {
        return 0, err
    }
    return lines, nil
}



// 改進後
func Countlines(r io.Reader) (int, error) {
    sr := bufio.NewScanner(r)
    lines := 0
    for sr.Scan() {
        lines ++
    }
    return lines,sr.Err()
}

Wrap errors

傳統error的問題

還記得之前我們 auth 的程式碼吧,如果 Auto 返回錯誤,則 Aut0Request 會將錯誤返回給呼叫方,呼叫者可能也會這樣做,依此類推。在程式的頂部,程式的主體將把錯誤列印到螢幕或日誌檔案中,打印出來的只是:沒有這樣的檔案或目錄。

沒有生成錯誤的 file:line 資訊。沒有導致錯誤的呼叫堆疊的堆疊跟蹤。這段程式碼的作者將被迫進行長時間的程式碼分割,以發現是哪個程式碼路徑觸發了檔案未找到錯誤。

func AutoRusquest() (err error) {
  err = Anto()
  if err != nil {
    err = fmt.Errorf("auto failed:%v",err)
    return
  }  
  return
}

但是正如我們前面看到的,這種模式與 sentinel errors 或 type assertions 的使用不相容,因為將錯誤值轉換為字串,將其與另一個字串合併,然後將其轉換回 fmt.Errorf 破壞了原始錯誤,導致等值判定失敗。

你應該只處理一次錯誤。處理錯誤意味著檢查錯誤值,並做出單個決策。

func WriteAll(w io.Writer, buf []byte) {
    w.Write(buf)
}

我們經常發現類似的程式碼,在錯誤處理中,帶了兩個任務: 記錄日誌並且再次返回錯誤。

func WriteAll(w io.Writer, buf []byte) error {
    _, err := w.Write(buf)
    if err != nil {
        log.Panicln("write buf failed:", err)
        return err
    }
    return nil
}

在這個例子中,如果在 w.Write 過程中發生了一個錯誤,那麼一行程式碼將被寫入日誌檔案中,記錄錯誤發生的檔案和行,並且錯誤也會返回給呼叫者,呼叫者可能會記錄並返回它,一直返回到程式的頂部。

func WriteConfig(w *io.Writer,config *Config) {
  buf, err := json.Marshal(conf)
  if err != nil {
    log.Printf("could not marshal config: %V", err)
    return err
  }

  if err := Writeall(w, buf); err != nil {
    log.Printf("could not write config: %v", err)
    return err
  }
}

func main() {
    err := Writeconfig(f, &conf)
    fmt.Println(err)
}

/*
unable to write: io.EOF
could not write config: io.EOF
*/

Go 中的錯誤處理契約規定,在出現錯誤的情況下,不能對其他返回值的內容做出任何假設。由於 JSON 序列化失敗,buf 的內容是未知的,可能它不包含任何內容,但更糟糕的是,它可能包含一個半寫的 JSON 片段。

由於程式設計師在檢查並記錄錯誤後忘記 return,損壞的緩衝區將被傳遞給 WriteAll,這可能會成功,因此配置檔案將被錯誤地寫入。但是,該函式返回的結果是正確的。

棧處理錯誤

日誌記錄與錯誤無關且對除錯沒有幫助的資訊應被視為噪音,應予以質疑。記錄的原因是因為某些東西失敗了,而日誌包含了答案。

  • 錯誤要被日誌記錄。

  • 應用程式處理錯誤,保證100%完整性。

  • 之後不再報告當前錯誤。

包:github.com/pkg/errors

func main() {
    _, err := Readconfig()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
}

func Readfile(path string) ([]byte, error) {
    f, err := os.Open(path)
    if err != nil {
        return nil, errors.Wrap(err, "open failed")
    }

    defer f.Close()
    buf, err := ioutil.ReadAll(f)
    if err != nil {
        return nil, errors.Wrap(err, "read failed")
    }

    return buf, nil
}

func Readconfig() ([]byte, error) {
    home := os.Getenv("HOME")
    config, err := Readfile(filepath.Join(home, "settings.xml"))
    return config, errors.WithMessage(err, "could not read config")
}

/*
could not read config: open failed: open /Users/zhaohaiyu/settings.xml: no such file or directory
exit status 1
*/
func main() {
    _, err := Readconfig()
    if err != nil {
        fmt.Printf("original error: %T -> %v\n", errors.Cause(err), errors.Cause(err))
        fmt.Printf("stack trace: \n%+v\n", err)
        os.Exit(1)
    }
}

/*
original error: *os.PathError -> open /Users/zhaohaiyu/settings.xml: no such file or directory
stack trace: 
open /Users/zhaohaiyu/settings.xml: no such file or directory
open failed
main.Readfile
        /Users/zhaohaiyu/code/test/main.go:35
main.Readconfig
        /Users/zhaohaiyu/code/test/main.go:51
main.main
        /Users/zhaohaiyu/code/test/main.go:22
runtime.main
        /usr/local/Cellar/go/1.15.3/libexec/src/runtime/proc.go:204
runtime.goexit
        /usr/local/Cellar/go/1.15.3/libexec/src/runtime/asm_amd64.s:1374
could not read config
exit status 1
*/

通過使用 pkg/errors 包,您可以向錯誤值新增上下文,這種方式既可以由人也可以由機器檢查。

errors.Wrap(err, "read failed")

wrap errors使用

  1. 在你的應用程式碼中,使用 errors.New 或者 errros.Errorf 返回錯誤。
func parseargs(args []string) error {
    if len(args) < 3 {
        return errors.Errorf("not enough arguments, expected at Least")
    }

    // ...

    return nil
}
  1. 如果呼叫其他的函式,通常簡單的直接返回。
if err != nil {
  return err
}
  1. 如果和其他庫進行協作,考慮使用 errors.Wrap 或者 errors.Wrapf 儲存堆疊資訊。同樣適用於和標準庫協作的時候。
f, err := os.Open(file)
if err != nil {
    return errors.Wrapf(err, "open %s failed",file)
}
  1. 直接返回錯誤,而不是每個錯誤產生的地方到處打日誌。

  2. 在程式的頂部或者是工作的 goroutine 頂部(請求入口),使用 %+v 把堆疊詳情記錄。

func main() {
    err := app.Run()
    if err != nil {
        fmt.Printf("FATAL:%+v\n", err)
        os.Exit(1)
    }
}
  1. 使用 errors.Cause 獲取 root error,再進行和 sentinel error 判定。

總結:

  • Packages that are reusable across many projects only return root error values.(選擇 wrap error 是隻有 applications 可以選擇應用的策略。具有最高可重用性的包只能返回根錯誤值。此機制與 Go 標準庫中使用的相同(kit 庫的 sql.ErrNoRows)。)

  • If the error is not going to be handled, wrap and return up the call stack.(這是關於函式/方法呼叫返回的每個錯誤的基本問題。如果函式/方法不打算處理錯誤,那麼用足夠的上下文 wrap errors 並將其返回到呼叫堆疊中。例如,額外的上下文可以是使用的輸入引數或失敗的查詢語句。確定您記錄的上下文是足夠多還是太多的一個好方法是檢查日誌並驗證它們在開發期間是否為您工作。)

  • Once an error is handled, it is not allowed to be passed up the call stack any longer.( 一旦確定函式/方法將處理錯誤,錯誤就不再是錯誤。如果函式/方法仍然需要發出返回,則它不能返回錯誤值。它應該只返回零(比如降級處理中,你返回了降級資料,然後需要 return nil)。)

Go1.13 error

1
函式在呼叫棧中新增資訊向上傳遞錯誤,例如對錯誤發生時發生的情況的簡要描述。

if err != nil {
    return fmt.Errorf("decompress %v:%v", name, err)
}

使用建立新錯誤 fmt.Errorf 丟棄原始錯誤中除文字外的所有內容。正如我們在上面的QueryError 中看到的那樣,我們有時可能需要定義一個包含底層錯誤的新錯誤型別,並將其儲存以供程式碼檢查。這裡是 QueryError:

type QueryError struct {
    Query string
    Err   error
}

程式可以檢視 QueryError \p值以根據底層錯誤做出決策。

if e, ok := err.(*Queryerror); ok && e.Err == ErrPermission {
    //query failed because of a permission problem
}

go1.13為 errors 和 fmt 標準庫包引入了新特性,以簡化處理包含其他錯誤的錯誤。其中最重要的是: 包含另一個錯誤的 error 可以實現返回底層錯誤的 Unwrap 方法。如果 e1.Unwrap() 返回 e2,那麼我們說 e1 包裝 e2,您可以展開 e1 以獲得 e2。

按照此約定,我們可以為上面的 QueryError 型別指定一個 Unwrap 方法,該方法返回其包含的錯誤:

func (e *Queryerror) Unwrap() error { return e.Err }

go1.13 errors 包包含兩個用於檢查錯誤的新函式:Is 和 As。

// Similar to:
// if err = Errnotfound {...}
if errors.Is(err, Errnotfound) {
    // something wasnt found
}

// Similar to
// if e, ok := err.(*Queryerror); ok {...}
var e *Queryerror
// Note: *Queryerror is the type of the error
if errorsAs(err, &e) {
    // err is a *Queryerror, and e is set to the errors value
}

Wrapping errors with %w

如前所述,使用 fmt.Errorf 向錯誤新增附加資訊。

if err != nil {
        return fmt.Errorf("decompress %v:%v", name, err)
}

在 Go 1.13中 fmt.Errorf 支援新的 %w 謂詞。

if err != nil {
        return fmt.Errorf("decompress %v:%w", name, err)
}

用 %w 包裝錯誤可用於 errors.Is 以及 errors.As

err := fmt.Errorf("access denied: % W", Errpermission)
if errors.Is(err, Errpermission) {
    // ...
}

Go2介紹

https://go.googlesource.com/proposal/+/master/design/29934-error-values.md

參考文章