Golang異常處理

從error的定義說起

type error interface {
	Error() string
}

Go 的error類型是一個接口。在Go中,只要實現了接口約定的方法,就等同於實現了這個接口。在日常的業務代碼編寫中,我們經常使用 errors 包下的New 方法來生成一個error對象。

func main() {
	err := errors.New("a error")
	fmt.Println(reflect.TypeOf(err))//*errors.errorString
}

可以發現,err 是一個指針類型,爲什麼這裏的 err 需要是一個指針呢?

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

查看errors包的代碼,我們知道返回指針是爲了確保err的唯一性。

以下的代碼是返回一個變量會引起的問題。

type ValueError string

func (ve ValueError) Error() string {
	return string(ve)
}
func New(text string) error {
	return ValueError(text)
}

func main() {
	simpleError := New("error")
	complexError := New("error")

	if simpleError == complexError {
		fmt.Println("true")//true
	}
}

Panic 機制

Go 沒有像其它語言一樣提供 try...catch機制。在Java代碼中,我們常見的就是寫了一大段的邏輯,然後在外層進行try...catch 異常處理。在Go中,我們只有 error 和 panic 函數,這裏主要介紹一下panic函數,通常我們是在程序碰見無法處理問題時纔會考慮panic,比如除數爲0了,這時候程序是會直接奔潰的。所以Go提供了 recover 函數,用來回復程序拋出的panic,我們可以在 recover 裏進行日誌、堆棧信息的記錄,便於後續問題的排查。

通常是在 defer 裏進行 recover 的。

package main

import "fmt"

func main() {
	f()
	fmt.Println("Returned normally from f.")
}
func f() {
	defer func() {
		if r := recover(); r != nil {
			fmt.Println("Recovered in f", r)
		}
	}()
	panic("panic")
}

web 服務一般會在最外層使用 recover 來避免因爲異常導致程序奔潰的情況。比如 gin.Default() 這個方法中就默認使用了 recovery() 中間件。

以下代碼片段來自 gin 的recovery 方法

return func(c *Context) {
		defer func() {
			if err := recover(); err != nil {
				// Check for a broken connection, as it is not really a
				// condition that warrants a panic stack trace.
				var brokenPipe bool
				if ne, ok := err.(*net.OpError); ok {
					if se, ok := ne.Err.(*os.SyscallError); ok {
						if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") {
							brokenPipe = true
						}
					}
				}
				....
		}()
		c.Next()
	}

這裏要注意的是,很多人利用defer-recover這樣的套路,實現了Go語言的'try-catch',這其實是不太優雅的。個人覺得這有點違背 Go 異常處理的設計理念。

錯誤類型(error types)

預定義錯誤

io 包中就定義了很多預定義的錯誤。最常見的可能就是EOF了。

var ErrShortBuffer = errors.New("short buffer")
var EOF = errors.New("EOF")
var ErrUnexpectedEOF = errors.New("unexpected EOF")

使用這種方式一個缺點就是不夠靈活,當我們需要使用預定錯誤的時候,我們通常需要通過判斷錯誤的類型是否相匹配

而在業務層中,我們通常返回錯誤的時候需要帶上一些上下文信息,方便後續問題的排查。如果我們使用fmt.Errorf(),就會破壞調用者的類型判斷。同時如果在業務層使用了預定義的錯誤,這時這個錯誤也必須是公共的,這將增加API的對外暴露的信息。同時調用方需要引用定義錯誤的這個包,增加了源代碼層面的依賴關係。所以這個類型的錯誤一般是在標準庫或者基礎庫中進行使用。

自定義錯誤

type MyError struct {
	When time.Time
	What string
}

func (e *MyError) Error() string {
	return fmt.Sprintf("%v : %v\n", e.When, e.What)
}

func test() error {
	return &MyError{When: time.Now(), What: "test error"}
}

func main() {
	err := test()
	switch err.(type) {
	case nil:
		fmt.Println("nil")
	case *MyError:
		fmt.Println("MyError")
	default:
		fmt.Println("unKnow")
	}
}

代碼中通過斷言轉換這個類型。與特定錯誤相比,自定義的錯誤可以攜帶更多的上下文信息,但是本質上仍需要error類型爲public。同樣和調用者有強耦合。

因此,使用我們要儘量避免在公共API中使用 error types。

Assert errors for behaviour, not type

我們應該斷言錯誤的特定行爲,而不是它的類型。這個建議來自於 Dave

調用方關注更多的地方是這個錯誤的行爲,而不是這個錯誤的類型,所以提供方可以封裝出特定錯誤類型的方法,只對外暴露這個方法而不暴露錯誤的類型。

type timeout interface {
	Timeout() bool
}
type timeError struct {
}

func (t *timeError) Error() string {
	return "the err is timeout"
}

func (t *timeError) Timeout() bool {
	return true
}

func PublicFunc() error {
	return &timeError{}
}

func testError(err error) {
	// fmt.Println("error type:", reflect.TypeOf(err))
	e, ok := err.(timeout)
	if ok {
		fmt.Println(e.Timeout())
	} else {
		fmt.Println("false")
	}
}

func main() {
	testError(errors.New("hhhhh"))//false
	testError(PublicFunc())//true
}

錯誤處理

在Go中,我們經常寫出這樣的代碼

if err != nil {
   //do someting
   //return 
}

從程序的嚴謹性來講,有錯誤的地方都是需要處理的。錯誤僅需要處理一次,如果認爲需要交給調用者處理,則僅需要將錯誤信息返回。

pkg/errors

GitHub地址

pkg/errors 是一個好用的第三方error包,兼容Go標準庫,提供了一些非常有用的操作用於封裝和處理錯誤。

type RawError struct {
	msg string
}

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

func main() {
	rawError := &RawError{msg: "no such file or directory"}
	fmt.Println("rawError:", rawError)
	wrapError := errors.Wrap(rawError, "a error occurred in xxxx,xxxxx")
	fmt.Println("wrapError:", wrapError)

	wrapwrapError := errors.Wrap(rawError, "double wrap")

	switch errors.Cause(wrapError).(type) {
	case *RawError:
		fmt.Println("the error type is *RawError ")
	default:
		fmt.Println("unknown")
	}

	switch errors.Cause(wrapwrapError).(type) {
	case *RawError:
		fmt.Println("the error type is *RawError ")
	default:
		fmt.Println("unknown")
	}
}

// rawError: no such file or directory
// wrapError: a error occurred in xxxx,xxxxx: no such file or directory
// the error type is *RawError
// the error type is *RawError

Go標準庫

Go1.13爲 errors 和 fmt 標準庫包引入了新特性。

// Unwrap returns the result of calling the Unwrap method on err, if err's
// type contains an Unwrap method returning error.
// Otherwise, Unwrap returns nil.
func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

// Is reports whether any error in err's chain matches target.
//
// The chain consists of err itself followed by the sequence of errors obtained by
// repeatedly calling Unwrap.
func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		// TODO: consider supporting target.Is(err). This would allow
		// user-definable predicates, but also may allow for coping with sloppy
		// APIs, thereby making it easier to get away with them.
		if err = Unwrap(err); err == nil {
			return false
		}
	}
}

// As finds the first error in err's chain that matches target, and if so, sets
// target to that error value and returns true. Otherwise, it returns false.
func As(err error, target interface{}) bool {
	if target == nil {
		panic("errors: target cannot be nil")
	}
	val := reflectlite.ValueOf(target)
	typ := val.Type()
	if typ.Kind() != reflectlite.Ptr || val.IsNil() {
		panic("errors: target must be a non-nil pointer")
	}
	targetType := typ.Elem()
	if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	for err != nil {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(interface{}) bool }); ok && x.As(target) {
			return true
		}
		err = Unwrap(err)
	}
	return false
}

//示例
var rawError = errors.New("simple error")
func SimpleError() error {
	return rawError
}
func main() {
	simpleError := SimpleError()

	wrapE := fmt.Errorf("wrap error:%w", simpleError)
	wrapWrapE := fmt.Errorf("wrapWrapE error:%w", wrapE)

	fmt.Printf("err:%+v\n", wrapWrapE)
	fmt.Printf("errors.Unwrap(wrapWrapE):%+v\n", errors.Unwrap(wrapWrapE))
	fmt.Printf("errors.Unwrap(errors.Unwrap(wrapWrapE)):%+v\n", errors.Unwrap(errors.Unwrap(wrapWrapE)))

	fmt.Println("errors.Is(err, rawError):", errors.Is(wrapWrapE, rawError))

	var errorValue error
	fmt.Println("errors.As(err, &rawError):", errors.As(wrapWrapE, &errorValue))
	fmt.Println("errorValue:", errorValue)

}

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