error的處理方式

重遠發表於2021-04-12

前言:
此文是觀看毛劍老師的《GO進階訓練營》中的《異常處理》的個人理解和總結,如果有不對的地方,還望指正,謝謝?。

error和exception

exception無法區分普通異常和致命異常,始終需要使用try...catch命令去捕捉異常,在自己編碼中經常會去考慮是否對其異常捕獲,甚至有時會對異常catch多次。

go中的error利用多返回值,使用普通errorpanic分別代表了普通異常和致命異常,更加明確了對異常的處理。

一、什麼是error

go中的error實際上是實現了error介面的普通型別,這樣可以很容易實現自定義的error

// https://golang.org/src/builtin/builtin.go
type error interface {
    Error() string
}

官方標準庫中的errors

// https://golang.org/src/errors/errors.go
package errors
func New(text string) error {
    return &errorString{text}
}

type errorString struct {
    s string
}

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

其中New方法返回errorString記憶體地址是為了避免在使用相同errors包時,不同的錯誤因為傳入的文字資訊相同而相等。

package main

import "fmt"

type myError struct {
    s string
}

func (m myError) Error() string {
    return m.s
}

func New(text string) error {
    return myError{text}
}

func main() {
    aErr := New("error")
    bErr := New("error")
    if aErr == bErr {
        fmt.Println("aErr == bErr") // aErr == bErr
    }
}

上述程式碼會認為分別New出來的error時相等的,這樣明顯是不正確的,當修改返回的error是記憶體地址時,即使相同文字也不會相等。

二、error和panic使用場景

error一般用於可遇見的普通異常,比如引數錯誤、查詢錯誤等異常;而panic一般用於強依賴的第三方、配置資訊發生錯誤,比如db無法連線、配置引數不存在等異常,但是使用方式需要根據實際程式碼邏輯進行判斷。

三、error的幾種使用方式

1、Sentinal Error

Sentinal Error是預先在包中定義的特定錯誤,比如io包中的EOF

// https://golang.org/src/io/io.go
var EOF = errors.New("EOF")

io.EOF是用於在檔案讀取時沒有內容返回的錯誤,所以在外部接收到error時需要判斷是否等於io.EOF

if err != nil {
    if err == io.EOF {
        // do something
    }
}

使用Sentinal Error有以下不足:

  • 無法攜帶多餘的上下文資訊,如果使用fmt.Errorf新增自定義資訊,返回的錯誤無法再與Sentinal Error進行比較。

  • 使包與包之間產生耦合,假如存在Sentinal Error的包是專案的基礎包,引入這個基礎的其他包都必須匯入這些錯誤值,增加了耦合性的同時基礎包也無法再對這些錯誤值進行相關修改。

所以Sentinal Error儘量避免在基礎包(或者很多包引用的包)中使用。

2、Error Type

Error Type是實現了error介面的自定義型別,可以保留底層的錯誤同時增加上下文資訊。

例如myError型別記錄了檔案和行號的上下文:

package main

import (
    "fmt"
)

type myError struct {
    s string
    file string
    line int
}

func (m *myError) Error() string {
    return fmt.Sprintf("%s-%d: %s", m.file, m.line, m.s)
}

func New(text, file string, line int) error {
    return &myError{text, file, line}
}

func main() {
    err := do()
    switch err := err.(type) {
    case nil:
        // no error
    case *myError:
        fmt.Printf("error occurred: %v\n", err.Error())
    default:
        // unknown error
    }
}

func do() error {
    return New("something error", "main.go", 27)
}

Error Type可以使用斷言轉換成這個型別,來獲取上下文資訊,但是Error Type仍然存在與其他包產生耦合的問題。

3、Opaque errors(非透明錯誤處理)

Opaque erros只關心返回的錯誤而不是錯誤內容。

func fn() error {
    i, err := do()
    if err != nil {
        return err
    }
    // do something
}

Opaque errors的好處減少了程式碼間的耦合性,但是在很多時候仍然需要確認返回錯誤的性質,以便進一步處理,比如網路請求是超時還是返回錯誤,此時推薦使用斷言錯誤的行為,而不是斷言錯誤的型別。

例如:
定義timeout介面,IsTimeout方法判斷錯誤的行為。

type timeout interface {
    Timeout() bool
}

func IsTimeout(err error) bool {
    t, ok := err.(timeout)
    return ok && t.Timeout()
}

當相關error實現Timeout方法後即可判斷其存在超時行為。

if IsTimeout(err) {
    // time out
}

這裡關鍵,可以在不到入定義錯誤的包或者不瞭解底層型別時判斷錯誤的性質。

4、Wrap Error

Wrap Error可以將最底層的錯誤儲存的同時新增更多上下文資訊,需要判斷錯誤性質時可以獲取底層錯誤。

Wrap Error目前有兩種方式,一種是標準庫的errors

包,另外一種就是第三方包github.com/pkg/errors(下稱pkg/errors),在目前go 1.x版本下,pkg/errors相比於errors包可以記錄堆疊資訊,方便除錯。

package main

import (
    "errors"
    "fmt"
    pkError "github.com/pkg/errors"
)

func main() {
    err := service()
    fmt.Printf("casue err: %+v\n", pkError.Cause(err))
    fmt.Printf("err: %+v", err)
}

func service() error {
    err := biz()
    return pkError.WithMessage(err, "this is service")
}

func biz() error {
    err := dao()
    return pkError.WithMessage(err, "this is biz")
}

func dao() error {
    err := errors.New("dao err")
    return pkError.Wrap(err, "this is dao")
}

// 列印資訊
// casue err: dao err
// err: dao err
// this is dao
// main.dao
// .../main.go:26 main.biz
// .../main.go:20 main.service
// .../main.go:15 main.main
// .../main.go:10 runtime.main
// /usr/local/go/src/runtime/proc.go:225 runtime.goexit
// /usr/local/go/src/runtime/asm_amd64.s:1371
// this is biz
// this is service

這裡假設dao層獲取對資料的操作錯誤後,使用pkg/errors將錯誤Wrap後返回,biz層獲取到dao層錯誤使用WithMessage增加相關上下文,最終傳遞上層可列印出上下文和堆疊資訊,並且可以使用Cause(或者Unwrap)獲取最底層錯誤。

當需要比較錯誤時,可使用IsAs方法,如果錯誤已經被Wrap,它們會獲取最底層錯誤進行比較。

func Is(err, target error) bool
func As(err, target interface{}) bool

As相比IsAs會在err的底層錯誤與target相等時,將err賦值給target

Wrap Error小結:
Wrap Error可以保留Sentinal Error特性的同時攜帶上下文資訊,一定程度上也相容了Error Type特性,在日常編碼中推薦使用這種方式。

注意事項:

1. 如果函式返回了value,error,必須判定error,否則返回的value不可用,除非你連value也不關心
2. error應該只被處理一次(列印日誌也算是對error的一種處理),要麼降級處理,要麼向上層返回error
3. 當與第三方庫或者基礎庫互動時,可以將第一次錯誤Wrap(只能Wrap一次,否則將重複列印堆疊資訊)向上返回,最終在頂層列印日誌,可以避免日誌的重複列印及除錯方便。
4.使用哪種error的處理方式,最終由使用場景來決定。

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章