golang中的錯誤處理

2022-11-02 15:00:30

0.1、索引

https://waterflow.link/articles/1666716727236

1、panic

當我們執行panic的時候會結束下面的流程:

package main

import "fmt"

func main() {
	fmt.Println("hello")
	panic("stop")
	fmt.Println("world")
}
go run 9.go 
hello
panic: stop

但是panic也是可以捕獲的,我們可以使用defer和recover實現:

package main

import "fmt"

func main() {

	defer func() {
		if r := recover(); r != nil {
			fmt.Println("recover: ", r)
		}
	}()

	fmt.Println("hello")
	panic("stop")
	fmt.Println("world")
}
go run 9.go
hello
recover:  stop

那什麼時候適合panic呢?在 Go 中,panic 用於表示真正的異常,例如程式錯誤。我們經常會在一些內建包裡面看到panic的身影。

比如strings.Repeat重複返回一個由字串 s 的計數副本組成的新字串:

func Repeat(s string, count int) string {
	if count == 0 {
		return ""
	}

	// 
	if count < 0 {
		panic("strings: negative Repeat count")
	} else if len(s)*count/count != len(s) {
		panic("strings: Repeat count causes overflow")
	}

	...
}

我們可以看到當重複的次數小於0或者重複count次之後s的長度溢位,程式會直接panic,而不是返回錯誤。這時因為strings包限制了error的使用,所以在程式錯誤時會直接panic。

還有一個例子是關於正規表示式的例子:

package main

import (
	"fmt"
	"regexp"
)

func main() {
	pattern := "a[a-z]b*" // 1
	compile, err := regexp.Compile(pattern) // 2
	if err != nil { // 2
		fmt.Println("compile err: ", err)
		return
	}
  // 3
	allString := compile.FindAllString("acbcdadb", 3)
	fmt.Println(allString)

}
  1. 編寫一個正規表示式
  2. 呼叫Compile,解析正規表示式,如果成功,返回用於匹配文字的 Regexp 物件。否則返回錯誤
  3. 利用正則,在輸入的字串中,獲取所有的匹配字元

可以看到如果上面正則解析失敗是可以繼續往下執行的,但是regexp包中還有另外一個方法MustCompile:

func MustCompile(str string) *Regexp {
	regexp, err := Compile(str)
	if err != nil {
		panic(`regexp: Compile(` + quote(str) + `): ` + err.Error())
	}
	return regexp
}

這個方法說明正則的解析是強依賴的,如果解析錯誤,直接panic結束程式。使用者可以根據實際情況選擇。

但是實際開發中我們還是要謹慎使用panic,因為它會使程式結束執行(除非我們呼叫defer recover)

2、包裝錯誤

錯誤包裝是將錯誤包裝或者打包在一個包裝容器中,這樣的話我們就可以追溯到源錯誤。錯誤包裝的主要作用就是:

  1. 為錯誤新增上下文
  2. 將錯誤標記為特定型別的錯誤

我們可以看一個存取資料庫的例子:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}

func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, errors.Wrap(err, "六月的想存取這個課件") // 2
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied") // 1
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		fmt.Println(err)
	}
}
  1. 存取資料庫時我們返回了原始的錯誤資訊
  2. 到上層我們新增了一些自定義的上下文資訊
go run 9.go
六月的想存取這個課件: permission denied

當然我們也可以將錯誤包裝成我們自定義型別的錯誤,我們稍微修改下上面的例子:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}

// 1
type ForbiddenError struct {
	Err error
}

// 2
func (e *ForbiddenError) Error() string {
	return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, &ForbiddenError{err} // 4
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied") // 3
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		fmt.Println(err)
	}
}
  1. 首先我們自定義了ForbiddenError的錯誤型別
  2. 我們實現了error介面
  3. 存取資料庫丟擲原始錯誤
  4. 上層返回ForbiddenError型別的錯誤
go run 9.go
Forbidden: permission denied

當然我們也可以不用建立自定義錯誤的型別,去包裝錯誤新增上下文:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}


func getCourseware(id int64) (*Courseware, error) {
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, fmt.Errorf("another wrap err: %w", err) // 1
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied")
}

func main() {
	_, err := getCourseware(11)
	if err != nil {
		fmt.Println(err)
	}
}
  1. 使用%w包裝錯誤

使用這的好處是我們可以追溯到源錯誤,從而方便我們做一些特殊的處理。

還有一種方式是使用:

return nil, fmt.Errorf("another wrap err: %v", err)

%v的方式不會包裝錯誤,所以無法追溯到源錯誤,但往往有時候我們會選擇這種方式,而不用%w的方式。%w的方式雖然能包裝源錯誤,但往往我們會通過源錯誤去做一些處理,假如源錯誤被修改,那包裝這個源錯誤的相關錯誤都需要做響應變化。

3、錯誤型別判斷

我們擴充套件一下上面查詢課件的例子。現在我們有這樣的判斷,如果傳進來的id不合法我們返回400錯誤,如果查詢資料庫報錯我們返回500錯誤,我們可以像下面這樣寫:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
	if id <= 0 {
		return nil, fmt.Errorf("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, &ForbiddenError{err}
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied")
}

func main() {
	_, err := getCourseware(500) // 我們可以修改這裡的id看下列印的結構
	if err != nil {
		switch err := err.(type) {
		case *ForbiddenError:
			fmt.Println("500 err: ", err)
		default:
			fmt.Println("400 err: ", err)
		}
	}
}
go run 9.go
500 err:  Forbidden: permission denied

這樣看起來好像也沒什麼問題,現在我們稍微修改下程式碼,把上面ForbiddenError包裝一下:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
	if id <= 0 {
		return nil, fmt.Errorf("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, fmt.Errorf("wrap err: %w", &ForbiddenError{err}) // 這裡包裝了一層錯誤
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied")
}

func main() {
	_, err := getCourseware(500)
	if err != nil {
		switch err := err.(type) {
		case *ForbiddenError:
			fmt.Println("500 err: ", err)
		default:
			fmt.Println("400 err: ", err)
		}
	}
}
go run 9.go
400 err:  wrap err: Forbidden: permission denied

可以看到我們的Forbidden錯誤進到了400裡面,這並不是我們想要的結果。之所以會這樣,是因為在ForbiddenError的外面又包裝了一層Error錯誤,使用型別斷言的時候判斷出來的是Error錯誤,所以進到了400分支。

這裡我們可以使用errors.As方法,它會遞迴呼叫Unwrap方法,找到錯誤鏈中第一個與target匹配的方法:

package main

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

type Courseware struct {
	Id int64
	Code string
	Name string
}

type ForbiddenError struct {
	Err error
}

func (e *ForbiddenError) Error() string {
	return "Forbidden: " + e.Err.Error()
}

func getCourseware(id int64) (*Courseware, error) {
	if id <= 0 {
		return nil, fmt.Errorf("invalid id: %d", id)
	}
	courseware, err := getFromDB(id)
	if err != nil {
		return nil, fmt.Errorf("wrap err: %w", &ForbiddenError{err})
	}
	return courseware, nil
}

func getFromDB(id int64) (*Courseware, error) {
	return nil, errors.New("permission denied")
}

func main() {
	_, err := getCourseware(500)
	if err != nil {
		var f *ForbiddenError // 這裡實現了*ForbiddenError介面,不然會panic
		if errors.As(err, &f) { // 找到匹配的錯誤
			fmt.Println("500 err: ", err)
		} else {
			fmt.Println("400 err: ", err)
		}
	}
}
go run 9.go
500 err:  wrap err: Forbidden: permission denied

4、錯誤值判斷

在程式碼中或者mysql庫或者io庫中我們經常會看到這樣的全域性錯誤:

var ErrCourseware = errors.New("courseware")

這種錯誤我們稱之為哨兵錯誤。一般資料庫沒查到ErrNoRows或者io讀到了EOF錯誤,這些特定的錯誤可以幫助我們做一些特殊的處理。

一般我們會直接用==號判斷錯誤值,但是就像上面的如果錯誤被包裝哪我們就不好去判斷了。好在errors包中提供了errors.Is方法,通過遞迴呼叫Unwrap判斷錯誤鏈中是否與目標錯誤相匹配的錯誤值:

if err != nil {
    if errors.Is(err, ErrCourseware) {
        // ...
    } else {
        // ...
    }
}