Golang標(biāo)準(zhǔn)庫之errors包應(yīng)用方式
一. errors的基本應(yīng)用
errors包是一個比較簡單的包,包括常見的errors.New創(chuàng)建一個error對象,或通過error.Error方法獲取error中的文本內(nèi)容,本質(zhì)上在builtin類型中,error被定義為一個interface,這個類型只包含一個Error方法,返回字符串形式的錯誤內(nèi)容。
應(yīng)用代碼很簡單:
// 示例代碼 func Oops() error { return errors.New("iam an error") } func Print() { err := Oops() fmt.Println("oops, we go an error,", err.Error()) }
通過errors.New方法,可以創(chuàng)建一個error對象,在標(biāo)準(zhǔn)庫實現(xiàn)中,對應(yīng)了一個叫errorString的實體類型,是對error接口的最基本實現(xiàn)。
二. 錯誤類型的比較
代碼中經(jīng)常會出現(xiàn)err == nil 或者err == ErrNotExist之類的判斷,對于error類型,由于其是interface類型,實際比較的是interface接口對象實體的地址。
也就是說,重復(fù)的new兩個文本內(nèi)容一樣的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對象的比較,就能應(yīng)付大多數(shù)場景了,但是在大型系統(tǒng)中,內(nèi)置的error類型很難滿足需要,所以下面要講的是對error的擴展。
三. error的擴展
3.1 自定義error
go允許函數(shù)具有多返回值,但通常你不會想寫太多的返回值在函數(shù)定義上(looks ugly),而標(biāo)準(zhǔn)庫內(nèi)置的errorString類型由于只能表達字符串錯誤信息顯然受限。所以,可以通過實現(xiàn)error接口的方式,來擴展錯誤返回
// 自定義error類型 type EasyError struct { Msg string // 錯誤文本信息 Code int64 // 錯誤碼 } func (me *EasyError) Error() string { // 當(dāng)然,你也可以自定義返回的string,比如 // return fmt.Sprintf("code %d, msg %s", me.Code, me.Msg) return me.Msg } // Easy實現(xiàn)了error接口,所以可以在Oops中返回 func DoSomething() error { return &EasyError{"easy error", 1} } // 業(yè)務(wù)應(yīng)用 func DoBusiness() { err := DoSomething() e,ok := err.(EasyError) if ok { fmt.Printf("code %d, msg %s\n", e.Code, e.Msg) } }
現(xiàn)在在自定義的錯誤類型中塞入了錯誤碼信息。隨著業(yè)務(wù)代碼調(diào)用層層深入,當(dāng)最內(nèi)層的操作(比如數(shù)據(jù)庫操作)發(fā)生錯誤時,我們希望能在業(yè)務(wù)調(diào)用鏈上每一層都攜帶錯誤信息,就像遞歸調(diào)用一樣,這時可以用到標(biāo)準(zhǔn)庫的Unwrap方法
3.2 Unwrap與Nested error
一旦你的自定義error實現(xiàn)類型定義了Unwrap方法,那么它就具有了嵌套的能力,其函數(shù)原型定義如下:
// 標(biāo)準(zhǔn)庫Unwrap方法,傳入一個error對象,返回其內(nèi)嵌的error func Unwrap(err error) error // 自定義Unwrap方法 func (me *EasyError) Unwrap() error { // ... }
雖然error接口沒有定義Unwrap方法,但是標(biāo)準(zhǔn)庫的Unwrap方法中會通過反射隱式調(diào)用自定義類型的Unwrap方法,這也是業(yè)務(wù)實現(xiàn)自定義嵌套的途徑。我們給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} } // 可以很清楚的看到調(diào)用鏈上產(chǎn)生的錯誤信息 // 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中調(diào)用EasyError的Unwrap返回子error } }
輸出如下
$ ./sample
code 1, msg from DoSomething1
code 2, msg from DoSomething2
code 3, msg from DoSomething3
這樣就可以在深入的調(diào)用鏈中,通過嵌套的方式,將調(diào)用路徑中的錯誤信息,攜帶至調(diào)用棧的棧底。
對于不同模塊,返回的錯誤信息大不相同,比如網(wǎng)絡(luò)通信模塊期望錯誤信息攜帶http狀態(tài)碼,而數(shù)據(jù)持久層期望返回sql或redis commend,隨著模塊化的職能劃分,每個子模塊可能會定義自己的自定義error類型,這時在業(yè)務(wù)上去區(qū)分不同類別的錯誤,就可以使用Is方法
3.3 errors.Is方法與錯誤分類
以網(wǎng)絡(luò)錯誤和數(shù)據(jù)庫錯誤為例,分別定義兩種實現(xiàn)error接口的結(jié)構(gòu)NetworkError和DatabaseError。
// 網(wǎng)絡(luò)接口返回的錯誤類型 type NetworkError struct { Code int // 10000 - 19999 Msg string // 文本信息 Status int // http狀態(tài)碼 } // 數(shù)據(jù)庫模塊接口返回的錯誤類型 type DatabaseError struct { Code int // 20000 - 29999 Msg string // 文本錯誤信息 Sql string // sql string }
NetworkError與DatabaseError都實現(xiàn)了Error方法和Unwrap方法,代碼里就不重復(fù)寫了。錯誤類型的劃分,導(dǎo)致上層業(yè)務(wù)對error的處理產(chǎn)生變化:業(yè)務(wù)層需要知道發(fā)生了什么,才能給用戶提供恰當(dāng)?shù)奶崾?,但是又不希望過分詳細(xì),比如用戶期望看到的是“數(shù)據(jù)訪問異常”、“請檢查網(wǎng)絡(luò)狀態(tài)”,而不希望用戶看到“unknown column space in field list…”、“request timeout…”之類的技術(shù)性錯誤信息。此時Is方法就派上用場了。
現(xiàn)在我們?yōu)榫W(wǎng)絡(luò)或數(shù)據(jù)庫錯誤都增加一個Code錯誤碼,并且人為對錯誤碼區(qū)間進行劃分,[10000,20000)表示網(wǎng)絡(luò)錯誤,[20000,30000)表示數(shù)據(jù)庫錯誤,我們期望在業(yè)務(wù)層能夠知道錯誤碼中是否包含網(wǎng)絡(luò)錯誤或數(shù)據(jù)訪問錯誤,還需要為兩種錯誤類型添加Is方法:
var( // 將10000和20000預(yù)留,用于在Is方法中判斷錯誤碼區(qū)間 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方法隱式調(diào)用的,來看一下業(yè)務(wù)代碼
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("網(wǎng)絡(luò)異常") } else if errors.Is(err, ErrDatabases) { fmt.Println("數(shù)據(jù)訪問異常") } } else { fmt.Println("everything is ok") } return nil }
執(zhí)行DoBusiness,輸出如下:
$ ./sample
網(wǎng)絡(luò)異常
通過Is方法,可以將一批錯誤信息歸類,對應(yīng)用隱藏相關(guān)信息,畢竟大部分時候,我們不希望用戶直接看到出錯的sql語句。
3.4 errors.As方法與錯誤信息讀取
現(xiàn)在通過Is實現(xiàn)了分類,可以判斷一個錯誤是否是某個類型,但是更進一步,如果我們想得到不同錯誤類型的詳細(xì)信息呢?業(yè)務(wù)層拿到返回的error,就不得不通過層層Unwrap和類型斷言來獲取調(diào)用鏈中的深層錯誤信息。所以errors包提供了As方法,在Unwrap的基礎(chǔ)上,直接獲取error接口中,實際是error鏈中指定類型的錯誤。
所以在DatabaseError的基礎(chǔ)上,再定義一個RedisError類型,作為封裝redis訪問異常的類型
// Redis模塊接口返回的錯誤類型 type RedisError struct { EasyError Command string // redis commend Address string // redis instance address } func (re *RedisError) Error() string { return re.Msg }
在業(yè)務(wù)層,嘗試讀取數(shù)據(jù)庫和redis錯誤的詳細(xì)信息
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 } // 執(zhí)行業(yè)務(wù)代碼 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類型,可以實現(xiàn)自定義的error類型
- error支持鏈?zhǔn)降慕M織形式,通過自定義Unwrap實現(xiàn)對error鏈的遍歷
- errors.Is用于判定error是否屬于某類錯誤,歸類方式可以在自定義error的Is方法中實現(xiàn)
- errors.As同樣可以用于判斷error是否屬于某個錯誤,避免了顯式的斷言處理,并同時返回使用該類型錯誤表達的錯誤信息詳情
- 無論是Is還是As方法,都會嘗試調(diào)用Unwrap方法遞歸地查找錯誤,所以如果帶有Nesty的錯誤,務(wù)必要實現(xiàn)Unwrap方法才可以正確匹配
通過這些手段,可以在不侵入業(yè)務(wù)接口的情況下,豐富錯誤處理,這就是errors包帶來的便利。
總結(jié)
以上為個人經(jīng)驗,希望能給大家一個參考,也希望大家多多支持腳本之家。
相關(guān)文章
golang如何使用gos7讀取S7200Smart數(shù)據(jù)
文章介紹了如何使用Golang語言的Gos7工具庫讀取西門子S7200Smart系列PLC的數(shù)據(jù),通過指定數(shù)據(jù)塊號、起始字節(jié)偏移量和數(shù)據(jù)長度,可以精確讀取所需的數(shù)據(jù),感興趣的朋友跟隨小編一起看看吧2024-12-12sublime text3解決Gosublime無法自動補全代碼的問題
本文主要介紹了sublime text3解決Gosublime無法自動補全代碼的問題,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2022-01-01解析GOROOT、GOPATH、Go-Modules-三者的關(guān)系
這篇文章主要介紹了解析GOROOT、GOPATH、Go-Modules-三者的關(guān)系,本文給大家介紹的非常詳細(xì),對大家的學(xué)習(xí)或工作具有一定的參考借鑒價值,需要的朋友可以參考下2020-10-10