Go語言中錯誤處理的方式總結(jié)
Go 的 error 有兩個很重要的特性:
- error 就是一個普通的值,處理起來沒有額外的開銷
- error 的擴展性很不錯,可以按照不同的場景來自定義錯誤
在 Go1.13 之后,在 errors 包中提供了一些函數(shù),讓錯誤的處理和追蹤更加方便一些。
這篇文章會結(jié)合 errors 中的函數(shù),來討論一下 Go 中常見的 error 使用方式。
這里說的 errors 包是指 Go 中的原生 errors 包。
1、原生 error
在 Go 的錯誤處理中,下面的代碼占絕大多數(shù):
if err != nil { return err }
在滿足業(yè)務(wù)需求的情況下,這種錯誤處理其實是最推薦的方式,這種直接透傳的方式讓代碼之間的耦合度更低。在很多情況下,如果不關(guān)心錯誤中的具體信息,使用這種方式就可以了。
2、提前定義好 error
原生的 error 在有些情況下使用起來就不是很方便,比如我需要獲得具體的錯誤信息,如果還用上面的方式來使用error,可能會出現(xiàn)下面的代碼:
if err != nil && err.Error() == "invalid param" { }
寫過代碼的都知道上面的代碼很不優(yōu)雅,另外如果錯誤的信息變化之后,這里的代碼邏輯就會出錯,可以通過把錯誤定義成一個變量:
var ( ErrInvalidParam = errors.New("invalid param") )
那么上面的代碼就可以變成這樣:
if err != nil && err == ErrInvalidParam { }
如果一次性需要處理的錯誤比較多,還可以使用 switch 進行處理:
if err != nil { switch err { case ErrInvalidParam: return case ErrNetWork: return case ErrFileNotExist: return default: return } }
但是這種方式還不完美,因為 error 在傳遞的過程中,有可能會被包裝,以攜帶更多的堆棧信息,比如下面這樣:
if err != nil { // 在包裝錯誤的時候,這里格式化錯誤要使用%w return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err) }
假設(shè)上面被包裝的錯誤是 ErrInvalidParam,那么在調(diào)用的地方判斷錯誤,就不能使用下面的代碼:
if err != nil && err == ErrInvalidParam { }
為了解決這個問題, errors.Is 函數(shù)可以判斷被包裝的 error 中是否有預(yù)期的 error:
if errors.Is(err, ErrInvalidParam) { }
盡量使用 errors.Is 來替代對 error 的比較。
3、使用自定義的錯誤類型
上面的 error 使用方式在某些情況下還是不能滿足要求。假如對于上面的無效參數(shù) error,業(yè)務(wù)方想要知道具體是哪個參數(shù)無效,直接定義的錯誤就無法滿足要求。error 本質(zhì)是一個接口,也就是是說,只要實現(xiàn)了 Error 方法,就是一個 error 類型:
type error interface { Error() string }
那么就可以自定義一種錯誤類型:
type ErrInvalidParam struct { ParamName string ParamValue string } func (e *ErrInvalidParam) Error() string { return fmt.Sprintf("invalid param: %+v, value: %+v", e.ParamName, e.ParamValue) }
然后就可以使用類型斷言機制或者類型選擇機制,來對不同類型的錯誤進行處理:
e, ok := err.(*ErrInvalidParam) if ok && e != nil { }
同樣可以在 switch 中使用:
if err != nil { switch err.(type) { case *ErrInvalidParam: return default: return } }
在這里 error 同樣會存在被包裝的問題,而 errors.As 剛好可以用來解決這個問題,可以判斷出被包裝的錯誤中是否存在某個 error 類型:
var e *ErrInvalidParam if errors.As(err, &e) { }
4、更靈活的 error 類型
上面的方式已經(jīng)可以解決大部分場景的 error 處理了,但是在一些復(fù)雜的情況下,可能需要從錯誤中獲取更多的信息,還包含一定的邏輯處理。
在 Go 的 net 包中,有這樣的一個接口:
type Error interface { error Timeout() bool Temporary() bool }
在這個接口中,有兩個方法,這兩個方法會對這個錯誤類型進行處理,判斷是超時錯誤還是臨時錯誤,實現(xiàn)了這個接口的 error 要實現(xiàn)這兩個 方法,實現(xiàn)具體的判斷邏輯。
在處理具體 error 時,會調(diào)用相應(yīng)的方法來判斷:
if ne, ok := e.(net.Error); ok && ne.Temporary() { // 對臨時錯誤進行處理 }
if ne, ok := e.(net.Error); ok && ne.Timeout() { // 對超時錯誤進行處理 }
這種類型的 error 相對來說,使用的會比較少,一般情況下,盡量不要使用這么復(fù)雜的處理方式。
5、errors 中的其他能力
在 errors 包中,除了上面提到的 errors.Is 和 errors.As 兩個很有用的函數(shù)之外,還有一個比較實用的函數(shù)errors.Unwrap,這個函數(shù)可以從包裝的錯誤中將原錯誤解析出來。
可以使用 fmt.Errorf 來包裝 error,需要使用 %w 的格式化:
return fmt.Errorf("add error info: %+v, origin error: %w", "other info", err)
在后續(xù)的 error 處理時,可以調(diào)用 errors.Unwrap 函數(shù)來獲得被包裝前的 error:
err = errors.Unwrap(err) fmt.Printf("origin error: %+v\n", err)
package main import ( "errors" "fmt" ) func main(){ err1 := errors.New("zero") fmt.Printf("origin error: %+v\n", err1) err2 := fmt.Errorf("add error info: %+v, origin error: %w", "other info", err1) fmt.Printf("origin error: %+v\n", err2) err3 := errors.Unwrap(err2) fmt.Printf("origin error: %+v\n", err3) }
程序輸出
origin error: zero
origin error: add error info: other info, origin error: zero
origin error: zero
6、注意
如果需要使用 goroutine 時,應(yīng)該使用統(tǒng)一的 Go 函數(shù)進行創(chuàng)建,這個函數(shù)中會進行 recover ,避免因為野生goroutine panic 導致主進程退出。
func Go(f func()){ go func(){ defer func(){ if err := recover(); err != nil { log.Printf("panic: %+v", err) } }() f() }() }
在應(yīng)用程序中出現(xiàn)錯誤時,使用 errors.New 或者 errors.Errorf 返回錯誤:
errors.Errorf("用戶余額不足, uid: %d, money: %d", uid, money)
如果是調(diào)用應(yīng)用程序的其他函數(shù)出現(xiàn)錯誤,請直接返回,如果需要攜帶信息,請使用 errors.WithMessage。
// https://github.com/pkg/errors errors.WithMessage(err, "其他附加信息")
如果是調(diào)用其他庫(標準庫、企業(yè)公共庫、開源第三方庫等)獲取到錯誤時,請使用 errors.Wrap 添加堆棧信息。
切記,不要每個地方都是用 errors.Wrap 只需要在錯誤第一次出現(xiàn)時進行 errors.Wrap 即可。
根據(jù)場景進行判斷是否需要將其他庫的原始錯誤吞掉,例如可以把 repository 層的數(shù)據(jù)庫相關(guān)錯誤吞掉,返回業(yè)務(wù)錯誤碼,避免后續(xù)我們分割微服務(wù)或者更換 ORM 庫時需要去修改上層代碼。
注意我們在基礎(chǔ)庫,在大量引入的第三方庫編寫時一般不使用 errors.Wrap 避免堆棧信息重復(fù)。
func f() error { err := json.Unmashal(&a, data) if err != nil { return errors.Wrap(err, "其他附加信息") } // 其他邏輯 return nil }
禁止每個出錯的地方都打日志,只需要在進程的最開始的地方使用 %+v 進行統(tǒng)一打印,例如 http/rpc 服務(wù)的中間件。
錯誤判斷使用 errors.Is 進行比較:
func f() error { err := A() if errors.Is(err, io.EOF){ return nil } // 其他邏輯 return nil }
錯誤類型判斷,使用 errors.As 進行賦值:
func f() error { err := A() var errA errorA if errors.As(err, &errA){ } // 其他邏輯 return nil }
以上就是Go語言中錯誤處理的方式總結(jié)的詳細內(nèi)容,更多關(guān)于Go錯誤處理的資料請關(guān)注腳本之家其它相關(guān)文章!