前言
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.go
的AddrError
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裏面,使用指針實現接口有兩個主要用途:
- 爲了在實現該函數處可以修改指針調用者
- 大結構使用指針可以減小拷貝,另外可以保證共享,維持全局一個類型,類似於單例。
爲什麼實現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({"name":"bill"}), 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.13 的errors
提供了一個errors.Unwrap(w) 方法,返回原始錯誤,即被嵌套的那個error ,支持多次還原,直到返回nil。
總結
Go語言不像Java有Exception異常和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