聊一聊Go語言的error處理

前言

Go語言的錯誤處理是一個常見的操作,經常可以見到一個函數返回錯誤類型(error),後續通過if err != nil來判斷錯誤以及錯誤類型。這一次嘗試通過Go內置的error接口,聊一聊Go語言的錯誤處理以及Error的慣例用法。

Error接口

接口簽名

type error interface {
	Error() string
}

我們先看Go的src/builtin內置error接口,它只有一個Error()方法,返回一個string,用來備註錯誤信息。任何實現了這個方法的結構體都實現了error接口。

實現類

隨便列舉一個實現Error接口的結構體,比如Go1.12/src/net/net.goAddrError

type AddrError struct {
	Err  string
	Addr string
}

//使用指針接收器實現該Error()接口
func (e *AddrError) Error() string {
	if e == nil {
		return "<nil>"
	}
	s := e.Err
	if e.Addr != "" {
		s = "address " + e.Addr + ": " + s
	}
	return s
}

問題

爲什麼接口實現用指針接收器的場景?

在Go裏面,使用指針實現接口有兩個主要用途:

  1. 爲了在實現該函數處可以修改指針調用者
  2. 大結構使用指針可以減小拷貝,另外可以保證共享,維持全局一個類型,類似於單例。

爲什麼實現Error()方法使用指針接收器?

我們可以看到上面AddrError使用指針接收器AddrError實現 Error() 接口,結合上一個問題的用途分析,Error() 方法主要是爲了第二點,唯一標識錯誤類型。

在Go中,Error是一個可比較的接口,我們都知道,指針的比較是比較地址,如果通過結構體(值)比較,無法確定當前Error是自定義Error或者內置Error,如io.EOF。

通過這種方式,我們可以在err == io.EOF等於true的時候,大膽be sure這err不會是其他自定義Error實現類,一定來自於io包的內置error,這種內置錯誤更多作爲一個全局變量貫穿在Go程序中,類似於單例。

其次,在自定義錯誤中,通過指針的.(type)斷言,可以針對不同error類型進行判斷,執行多態處理,統一使用指針進行實現方便斷言判斷處進行歸納。可以在src/encoding/json/decode.go找到幾個常用錯誤類型。

程序示例:

下面是一個通過斷言switch作出不同處理的例子

var u user
err := json.Unmarshal([]byte({&quot;name&quot;:&quot;bill&quot;}), u)
switch e := err.(type) {
case *json.UnmarshalTypeError:
	log.Printf("UnmarshalTypeError: Value[%s] Type[%v]\n", e.Value, e.Type)
case *json.InvalidUnmarshalError:
	log.Printf("InvalidUnmarshalError: Type[%v]\n", e.Type)
default:
	log.Println(err)
}

如果是通過值實現Error()方法,在case判斷處,需要歸納*json.UnmarshalTypeError以及json.UnmarshalTypeError,因爲++通過值實現的函數調用方可以是指針或者是值。++

自定義Error

場景

Error需要包裝額外的信息,比如調用棧信息。可以創建自定義錯誤,並且把調用棧信息填入該error,下面通過列舉一個自定義Error的demo,並且嘗試在自定義Error中加入上下文信息。
上下文信息指的是對當前結構進行一些屬性關聯(如當前結構體類型,時間,情景要素等),可以封裝一個context屬性或者新增幾個所需屬性,這裏列舉幾個簡單自定義錯誤,添加不同場景的上下文數據以及調用棧。

程序實例:

新建三個自定義錯誤,分別適應不同的場景:類型錯誤/容量錯誤/時間錯誤,並且在TypeError嵌入我們需要的:

  • 上下文,這裏簡單用string作描述
  • 棧信息,可能用於追蹤程序的執行
  • Type字段,這裏用於後續本例調試
package main

import (
	"fmt"
	"reflect"
)

//自定義錯誤1
type TypeError struct {
	//上下文
	context string
	Type reflect.Type
	trace string
}

//具體Error()實現,返回類型,上下文,以及調用棧描述
func (tye* TypeError) Error() string {
	return fmt.Sprintf("Type of TypeError %s, contex: %s, trace[%s]",
		tye.Type, tye.context, tye.trace)
}

//自定義錯誤2
type SizeError struct {
	context string
	Type reflect.Type
}

func (sie * SizeError) Error() string {
	return fmt.Sprintf("SizeError context: %s", sie.context)
}

//自定義錯誤3
type UserError struct {
	context string
	Type reflect.Type
}

func (tie *UserError) Error() string {
	return fmt.Sprintf("UserError %s, contex: %s", tie.context)
}

執行棧的獲取示例:

/*
	https://www.komu.engineer/blogs/golang-stacktrace/golang-stacktrace
	獲取當前執行點的棧信息
*/
//Package errors provides ability to annotate you regular Go errors with stack traces.
func getStackTrace() string {
	stackBuf := make([]uintptr, 50)
	length := runtime.Callers(3, stackBuf[:])
	stack := stackBuf[:length]

	trace := ""
	frames := runtime.CallersFrames(stack)
	for {
		frame, more := frames.Next()
		trace = trace + fmt.Sprintf("\n\tFile: %s, Line: %d. Function: %s",
			frame.File, frame.Line, frame.Function)
		if !more {
			break
		}
	}
	return trace
}

生成錯誤示例:

/*
	模擬不同場景產生不同錯誤,使用入參instruction進行選擇
 */
func CreateWithDiffError(instruction string) error {
	switch instruction {
	case "typeErr":
		//上下文添加備註
		return &TypeError{"Lack of energy.", reflect.TypeOf(TypeError{}), getStackTrace()}
	case "sizeErr":
		//上下文添加時間信息
		return &SizeError{"time:" + time.UnixDate, reflect.TypeOf(SizeError{})}
	case "userErr":
		//上下文添加用戶
		return &UserError{"UserErr with selfContext: @pixelpig.",
			reflect.TypeOf(UserError{})}
	default:
		return errors.New("UnknownError")
	}	
}

上面提到,因爲Error的接口實現是通過指針實現的,所以可以通過.(type)進行類型斷言,針對不同錯誤類型進行處理。

另外,這裏的type是自定義Error結構體反射獲取的類型,與生成Error處賦值的Type屬性是兩個含義。

類型斷言場景:

func ParseErr(err error) {
	if err != nil {
		switch e := err.(type) {
		case *TypeError:
			log.Printf("ErrType[%v] Context[%s]\n, Trace[%s]\n", e.Type, e.context, e.trace)
			//TODO: Handle 類型錯誤
			break
		case *UserError:
			log.Printf("ErrType[%v] Context[%s]\n", e.Type, e.context)
			//TODO: Handle 用戶錯誤
			break
		case *SizeError:
			log.Printf("ErrType[%v] Context[%s]\n", e.Type, e.context)
			//TODO: Handle 容量錯誤
			break
		default:
			log.Println(err)
		}
	}
}

主程序:

var (
	TYE = "typeErr"
	SE = "sizeErr"
	UE = "userErr"
)

func main() {
	typeErr := CreateWithDiffError(TYE)
	seErr := CreateWithDiffError(SE)
	timeErr := CreateWithDiffError(UE)

	ParseErr(typeErr)
	ParseErr(seErr)
	ParseErr(timeErr)
}

程序輸出:

2020/02/02 13:08:25 ErrType[main.TypeError] Context[Lack of energy.]
, Trace[
	File: D:/goProject/src/HelloGo/basic/ErrorHandle/ErrorDemo.go, Line: 19. Function: main.main
	File: D:/Go1.12/src/runtime/proc.go, Line: 200. Function: runtime.main
	File: D:/Go1.12/src/runtime/asm_amd64.s, Line: 1337. Function: runtime.goexit]
2020/02/02 13:08:25 ErrType[main.SizeError] Context[time:Mon Jan _2 15:04:05 MST 2006]
2020/02/02 13:08:25 ErrType[main.UserError] Context[UserErr with selfContext: @pixelpig.]

可以看到第一個自定義類型打印出了程序執行的棧信息。

Wraping:error嵌套

如果不想通過自定義error來實現內嵌信息,在Go1.13以上版本,提供了一個新的錯誤包裝方式,通過擴展fmt.Errorf函數,加一個%w來生成一個可以包裝的錯誤,通過這種方式,我們可以創建一個嵌套Error。

示例:

/*
	包裝錯誤
 */
func WrapErr(err error) error{
	return fmt.Errorf("Wrap with shell: %v", err)
}

經過Errorf的包裝會返回一個新的Error,其中“Wrap with shell”是包裝的內容。

輸出如下:

2020/02/02 17:20:38 Wrap with shell: UserError contex: UserErr with selfContext: @pixelpig.

如果要還原解開錯誤,Go1.13errors提供了一個errors.Unwrap(w) 方法,返回原始錯誤,即被嵌套的那個error ,支持多次還原,直到返回nil。

總結

Go語言不像JavaException異常和JVM堆棧環境,沒有幫我們封裝執行棧的數據到內置error中,它建議程序員在錯誤發生處儘早處理,不傾向於把錯誤往上層拋,所以如果要方便追溯錯誤在程序的位置,可以通過生成自定義錯誤,植入函數棧的位置。

如果遵循Go的推薦實踐,大部分情況希望我們在錯誤發生處進行handle,儘早處理,那麼假如僅僅需要一些上下文信息,比如時間,用戶等。這種情況可以使用自定義error,或者使用Go自帶包裝方式,存儲我們額外需要的字段,如上述的UserError

參考鏈接

Custom errors in golang and pointer receivers
https://stackoverflow.com/questions/50333428/custom-errors-in-golang-and-pointer-receivers

Error Handling In Go, Part I
https://www.ardanlabs.com/blog/2014/10/error-handling-in-go-part-i.html
Error Handling In Go, Part II
https://www.ardanlabs.com/blog/2014/11/error-handling-in-go-part-ii.html

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