天天看點

Golang标準庫:errors包應用

一. errors的基本應用

errors包是一個比較簡單的包,包括常見的errors.New建立一個error對象,或通過error.Error方法擷取error中的文本内容,本質上在builtin類型中,error被定義為一個interface,這個類型隻包含一個Error方法,傳回字元串形式的錯誤内容。應用代碼很簡單

// 示例代碼
func Oops() error {
	return errors.New("iam an error")
}

func Print() {
	err := Oops()
	fmt.Println("oops, we go an error,", err.Error())
}
           

通過errors.New方法,可以建立一個error對象,在标準庫實作中,對應了一個叫errorString的實體類型,是對error接口的最基本實作。

二. 錯誤類型的比較

代碼中經常會出現err == nil 或者err == ErrNotExist之類的判斷,對于error類型,由于其是interface類型,實際比較的是interface接口對象實體的位址。

也就是說,重複的new兩個文本内容一樣的error對象,這兩個對象并不相等,因為比較的是這兩個對象的位址。這是完全不同的兩個對象

// 展示了error比較代碼
if errors.New("hello error") == errors.New("hello error") { // false
}
errhello := errors.New("hello error")
if errhello == errhello { // true
}
           

在通常的場景中,能掌握errors.New()、error.Error()以及error對象的比較,就能應付大多數場景了,但是在大型系統中,内置的error類型很難滿足需要,是以下面要講的是對error的擴充。

三. error的擴充

3.1 自定義error

go允許函數具有多傳回值,但通常你不會想寫太多的傳回值在函數定義上(looks ugly),而标準庫内置的errorString類型由于隻能表達字元串錯誤資訊顯然受限。是以,可以通過實作error接口的方式,來擴充錯誤傳回

// 自定義error類型
type EasyError struct {
	Msg  string	// 錯誤文本資訊
	Code int64	// 錯誤碼
}

func (me *EasyError) Error() string {
	// 當然,你也可以自定義傳回的string,比如
	// return fmt.Sprintf("code %d, msg %s", me.Code, me.Msg)
	return me.Msg
}

// Easy實作了error接口,是以可以在Oops中傳回
func DoSomething() error {
	return &EasyError{"easy error", 1}
}

// 業務應用
func DoBusiness() {
	err := DoSomething()
	e,ok := err.(EasyError)
	if ok {
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
	}
}
           

現在在自定義的錯誤類型中塞入了錯誤碼資訊。随着業務代碼調用層層深入,當最内層的操作(比如資料庫操作)發生錯誤時,我們希望能在業務調用鍊上每一層都攜帶錯誤資訊,就像遞歸調用一樣,這時可以用到标準庫的Unwrap方法

3.2 Unwrap與Nested error

一旦你的自定義error實作類型定義了Unwrap方法,那麼它就具有了嵌套的能力,其函數原型定義如下:

// 标準庫Unwrap方法,傳入一個error對象,傳回其内嵌的error
func Unwrap(err error) error

// 自定義Unwrap方法
func (me *EasyError) Unwrap() error {
	// ... 
}
           

雖然error接口沒有定義Unwrap方法,但是标準庫的Unwrap方法中會通過反射隐式調用自定義類型的Unwrap方法,這也是業務實作自定義嵌套的途徑。我們給EasyError增加一個error成員,表示包含的下一級error

// 
type EasyError struct {
	Msg  string	// 錯誤文字資訊
	Code int64	// 錯誤碼
	Nest error 	// 嵌套的錯誤
}

func (me *EasyError) Unwrap() error {
	return me.Nest
}

func DoSomething1() error {
	// ...
	err := DoSomething2()
	if err != nil {
		return &EasyError{"from DoSomething1", 1, err}
	}

	return nil
}

func DoSomething2() error {
	// ...
	err := DoSomething3()
	if err != nil {
		return &EasyError{"from DoSomething2", 2, err}
	}

	return nil
}

func DoSomething3() error {
	// ...

	return &EasyError{"from DoSomething3", 3, nil}
}
// 可以很清楚的看到調用鍊上産生的錯誤資訊
// Output:
// 	code 1, msg from DoSomething1
// 	code 2, msg from DoSomething2
// 	code 3, msg from DoSomething3
func main() {
	err := DoSomething1()
	for err != nil {
		e := err.(*EasyError)
		fmt.Printf("code %d, msg %s\n", e.Code, e.Msg)
		err = errors.Unwrap(err)		// errors.Unwrap中調用EasyError的Unwrap傳回子error
	}
}
           

輸出如下

$ ./sample

code 1, msg from DoSomething1

code 2, msg from DoSomething2

code 3, msg from DoSomething3

這樣就可以在深入的調用鍊中,通過嵌套的方式,将調用路徑中的錯誤資訊,攜帶至調用棧的棧底。

對于不同子產品,傳回的錯誤資訊大不相同,比如網絡通信子產品期望錯誤資訊攜帶http狀态碼,而資料持久層期望傳回sql或redis commend,随着子產品化的職能劃分,每個子子產品可能會定義自己的自定義error類型,這時在業務上去區分不同類别的錯誤,就可以使用Is方法

3.3 errors.Is方法與錯誤分類

以網絡錯誤和資料庫錯誤為例,分别定義兩種實作error接口的結構NetworkError和DatabaseError。

// 網絡接口傳回的錯誤類型
type NetworkError struct {
	Code   int	  // 10000 - 19999
	Msg    string // 文本資訊
	Status int    // http狀态碼
}

// 資料庫子產品接口傳回的錯誤類型
type DatabaseError struct {
	Code int	// 20000 - 29999
	Msg  string // 文本錯誤資訊
	Sql  string // sql string
}
           

NetworkError與DatabaseError都實作了Error方法和Unwrap方法,代碼裡就不重複寫了。錯誤類型的劃分,導緻上層業務對error的處理産生變化:業務層需要知道發生了什麼,才能給使用者提供恰當的提示,但是又不希望過分詳細,比如使用者期望看到的是“資料通路異常”、“請檢查網絡狀态”,而不希望使用者看到“unknown column space in field list…”、“request timeout…”之類的技術性錯誤資訊。此時Is方法就派上用場了。

現在我們為網絡或資料庫錯誤都增加一個Code錯誤碼,并且人為對錯誤碼區間進行劃分,[10000,20000)表示網絡錯誤,[20000,30000)表示資料庫錯誤,我們期望在業務層能夠知道錯誤碼中是否包含網絡錯誤或資料通路錯誤,還需要為兩種錯誤類型添加Is方法:

var(
	// 将10000和20000預留,用于在Is方法中判斷錯誤碼區間
	ErrNetwork  = &NetworkError{EasyError{"", 10000, nil}, 0}
	ErrDatabase = &DatabaseError{EasyError{"", 20000, nil}, ""}
)

func (ne NetworkError) Is(e error) bool {
	err, ok := e.(*NetworkError)
	if ok {
		start := err.Code / 10000
		return ne.Code >= 10000 && ne.Code < (start+1)*10000
	}
	return false
}

func (de DatabaseError) Is(e error) bool {
	err, ok := e.(*DatabaseError)
	if ok {
		start := err.Code / 10000
		return de.Code >= 10000 && de.Code < (start+1)*10000
	}
	return false
}
           

與Unwrap類似,Is方法也是被errors.Is方法隐式調用的,來看一下業務代碼

func DoNetwork() error {
	// ...
	return &NetworkError{EasyError{"", 10001, nil}, 404}
}

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoSomething() error {
	if err := DoNetwork(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

func DoBusiness() error {
	err := DoSomething()
	if err != nil {
		if errors.Is(err, ErrNetworks) {
			fmt.Println("網絡異常")
		} else if errors.Is(err, ErrDatabases) {
			fmt.Println("資料通路異常")
		}
	} else {
		fmt.Println("everything is ok")
	}
	return nil
}
           

執行DoBusiness,輸出如下:

$ ./sample

網絡異常

通過Is方法,可以将一批錯誤資訊歸類,對應用隐藏相關資訊,畢竟大部分時候,我們不希望使用者直接看到出錯的sql語句。

3.4 errors.As方法與錯誤資訊讀取

現在通過Is實作了分類,可以判斷一個錯誤是否是某個類型,但是更進一步,如果我們想得到不同錯誤類型的詳細資訊呢?業務層拿到傳回的error,就不得不通過層層Unwrap和類型斷言來擷取調用鍊中的深層錯誤資訊。是以errors包提供了As方法,在Unwrap的基礎上,直接擷取error接口中,實際是error鍊中指定類型的錯誤。

是以在DatabaseError的基礎上,再定義一個RedisError類型,作為封裝redis通路異常的類型

// Redis子產品接口傳回的錯誤類型
type RedisError struct {
	EasyError
	Command string // redis commend
	Address string // redis instance address
}

func (re *RedisError) Error() string {
	return re.Msg
}

           

在業務層,嘗試讀取資料庫和redis錯誤的詳細資訊

func DoDatabase() error {
	// ...
	return &DatabaseError{EasyError{"", 20003, nil}, "select 1"}
}

func DoRedis() error {
	// ...
	return &RedisError{EasyError{"", 30010, nil}, "set hello 1", "127.0.0.1:6379"}
}

func DoDataWork() error {
	if err := DoRedis(); err != nil {
		return err
	}
	if err := DoDatabase(); err != nil {
		return err
	}
	return nil
}

// 執行業務代碼
func DoBusiness() {
	err := DoDataWork()
	if err != nil {
		if rediserr := (*RedisError)(nil); errors.As(err, &rediserr) {
			fmt.Printf("Redis exception, commend : %s, instance : %s\n", rediserr.Command, rediserr.Address)
		} else if mysqlerr := (*DatabaseError)(nil); errors.As(err, &mysqlerr) {
			fmt.Printf("Mysql exception, sql : %s\n", mysqlerr.Sql)
		}
	} else {
		fmt.Println("everything is ok")
	}
}
           

運作DoBusiness,輸出如下

$ ./sample

Redis exception, commend : set hello 1, instance : 127.0.0.1:6379

conclusion

  • error是interface類型,可以實作自定義的error類型
  • error支援鍊式的組織形式,通過自定義Unwrap實作對error鍊的周遊
  • errors.Is用于判定error是否屬于某類錯誤,歸類方式可以在自定義error的Is方法中實作
  • errors.As同樣可以用于判斷error是否屬于某個錯誤,避免了顯式的斷言處理,并同時傳回使用該類型錯誤表達的錯誤資訊詳情
  • 無論是Is還是As方法,都會嘗試調用Unwrap方法遞歸地查找錯誤,是以如果帶有Nesty的錯誤,務必要實作Unwrap方法才可以正确比對

通過這些手段,可以在不侵入業務接口的情況下,豐富錯誤處理,這就是errors包帶來的便利。