錯誤處理的幾種套路 錯誤處理的幾種套路

錯誤處理的幾種套路

在Go語言中並不支持拋出異常的方式提示錯誤,而是通過支持多返回值的方式返回error接口、並且支持panic的方式在遇到致命錯誤時推出程序

致命錯誤

panic

func F()  {
    panic("panic")
}

func  main() {
    F()
}
panic: panic

goroutine 1 [running]:
main.F(...)
        /Users/zhangyu/go/src/myPrj/main.go:8
main.main()
        /Users/zhangyu/go/src/myPrj/main.go:12 +0x39

panic會終止整個程序並打印堆棧信息

異常保護

panic表示的是致命錯誤,但是在實際工程實踐中並不應該直接就終止程序,比如:web服務可能是部分接口存在問題另一部分還正常工作,如果直接退出程序會導致整個服務不可用並且無法降級處理

func F()  {
    panic("panic")
}

func  main() {
    defer func() {
        e := recover()
        fmt.Println("recover: ", e)
    }()
    F()
}
recover:  panic

通過recover方法就可以在出現panic錯誤時恢復程序,但是需要明確一點:不要嘗試恢復程序,因爲此時程序狀態已不可知了
常見場景就是在web服務中在出現panic時回覆500錯誤,而不是程序直接掛掉

普通錯誤

對於一般錯誤而言,Go語言中規範是通過在返回值的最後一個爲error接口來表示錯誤信息,但是在實際使用當中會有各種各樣的問題

errors.New

func F() error {
    return errors.New("f error")
}

func  main() {
    fmt.Println(F())
}
f error

這是最簡單的返回錯誤的方式,但是存在問題:

  • 調用者只能使用字符串比較函數來處理這些錯誤,在遇到格式化的問題時則束手無策

sentinel error

sentinel error指的是在包中預定義一些錯誤值然後調用方進行比較

//os/error.go
var (
    ErrInvalid = fs.ErrInvalid // "invalid argument"

    ErrPermission = fs.ErrPermission // "permission denied"
    ErrExist      = fs.ErrExist      // "file already exists"
    ErrNotExist   = fs.ErrNotExist   // "file does not exist"
    ErrClosed     = fs.ErrClosed     // "file already closed"

    ErrNoDeadline       = errNoDeadline()       // "file type does not support deadline"
    ErrDeadlineExceeded = errDeadlineExceeded() // "i/o timeout"
)

這是os包中的錯誤定義,在代碼中通常直接通過==來進行比較,但是會有一些問題:

  • 不能攜帶一些額外的錯誤信息,比如文件名之類的
  • 調用方必須在代碼中耦合這些錯誤值,包的暴露面變大同時不能隨意變更這些錯誤信息了

類型斷言

type Error struct {
    code int
    msg  string
}

func (e *Error) Error() string {
    return e.msg
}

func F() error {
    return &Error{code: 1,msg: "f error"}
}

func  main() {
    err := F()
    if e, ok := err.(*Error); ok {
        fmt.Println(e.msg, e.code)
    }
}

類型斷言的方式很簡單就是返回一個實現error接口的自定義的結構體,外部則根據結構體中的信息來處理,比較常見的方式可以定義錯誤碼
這樣導致的問題就是調用者和包存在較大的耦合關係

opaque error

opaque error的思想就是提供函數接口來判斷錯誤類型而不是直接操縱結構體字段

//包
type myerror struct {
    Code int
    Msg  string
}

func (e *myerror) Error() string {
    return e.Msg
}


func F() error {
    return &myerror{Code: 1,Msg: "f error"}
}

func IsError1(err error) bool {
    if e, ok := err.(*myerror); ok {
        if e.Code == 1 {
            return true
        }
    }
    return false
}
//主函數
func  main() {
    err := F()
    if IsError1(err) {
        fmt.Println("error 1")
    }
}

主要的思想就是對外提供函數而不是結構體,包的更新維護更加的方便

錯誤堆棧

在實際工作當中需要的也許不僅僅是錯誤信息還需要錯誤堆棧

package main

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

type MyError struct {
    Code int
    Msg  string
}

func (e *MyError) Error() string {
    return e.Msg
}


func F() error {
    e := &MyError{Code: 1,Msg: "f execute error"}
    return errors.Wrap(e, "[errors wrap]");
}

type stackTracer interface {
    StackTrace() errors.StackTrace
}

func  main() {
    e := F()
    var mye *MyError
    if  errors.As(e, &mye) {
        fmt.Println("mye: ", mye.Code, mye.Msg)
    }
    if err, ok := e.(stackTracer); ok {
        for _, f := range err.StackTrace() {
            fmt.Printf("%+s:%d\n", f, f)
        }
    }
}
mye:  1 f execute error
main.F
        /Users/zhangyu/go/src/myPrj/main.go:20
main.main
        /Users/zhangyu/go/src/myPrj/main.go:28
runtime.main
        /usr/local/Cellar/go/1.16.5/libexec/src/runtime/proc.go:225
runtime.goexit
        /usr/local/Cellar/go/1.16.5/libexec/src/runtime/asm_amd64.s:1371

github.com/pkg/errors包提供了很多增強方法用來包裝錯誤信息、類型斷言等等,在fmt包中的fmt.Errorf%w佔位符是類似的

type withMessage struct {
    cause error
    msg   string
}

本質就是額外包了一層結構體而已

總結

  這些方式在簡單的項目中怎麼樣的可以,但是在大的項目工程中就需要形成一定的規範

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章