Go版本大于1.13,程序里這樣做錯(cuò)誤處理才地道
大家好,這里是每周都在陪你進(jìn)步的網(wǎng)管。
之前寫過幾篇關(guān)于 Go 錯(cuò)誤處理的文章,發(fā)現(xiàn)文章里不少知識(shí)點(diǎn)都有點(diǎn)落伍了,比如Go在1.13后對(duì)錯(cuò)誤處理增加了一些支持,最大的變化就是支持了錯(cuò)誤包裝(Error Wrapping),以前想要在調(diào)用鏈路的函數(shù)里包裝錯(cuò)誤都是用"github.com/pkg/errors"這個(gè)庫。
Go 在2019年發(fā)布的Go1.13版本也采納了錯(cuò)誤包裝,并且還提供了幾個(gè)很有用的工具函數(shù)讓我們能更好地使用包裝錯(cuò)誤。這篇文章就來主要說一下這方面的知識(shí)點(diǎn),不過開始我們還是再次強(qiáng)調(diào)一下使用 Go Error 的誤區(qū),避免我們從其他語言切換過來時(shí)給自己后面挖坑。
自定義錯(cuò)誤要實(shí)現(xiàn)error接口
這一條估計(jì)很多人都知道,但是文章開頭開始先從這個(gè)慣例開始,因?yàn)槲乙郧按^一個(gè)PHP轉(zhuǎn)Go的研發(fā)團(tuán)隊(duì),可能大家一開始都不太會(huì),才有了這種錯(cuò)誤的使用方式。
首先我們再復(fù)述一遍,Go?通過error類型的值表示程序里的錯(cuò)誤。
error?類型是一個(gè)內(nèi)建接口類型,該接口只規(guī)定了一個(gè)返回字符串值的Error方法。
Go?程序的函數(shù)經(jīng)常會(huì)返回一個(gè)error值
調(diào)用者通過測試error?值是否是nil來進(jìn)行錯(cuò)誤處理。
error為nil?時(shí)表示成功;非nil的error表示失敗。
說完 Go? 里 error 最基本的使用方式后,接下來說項(xiàng)目里的自定義錯(cuò)誤類型。假如項(xiàng)目在 Dao 層定義了一個(gè)這樣的錯(cuò)誤類型來記錄數(shù)據(jù)庫查詢錯(cuò)誤。
假如,這個(gè)自定義的MyError?不去實(shí)現(xiàn)error?接口,Dao 層里的函數(shù)返回的都是MyError的話。
那么使用這些 Dao 函數(shù)的代碼邏輯層都得引入dao.MyError?這個(gè)額外的類型。有人會(huì)說,我把MyError?定義在公共包里,所有代碼邏輯層、Dao 層都用這個(gè)common.MyError總沒啥問題了吧。
使用上乍一看沒什么問題,但其實(shí)最大的問題就是不兼容、不符合Go語言對(duì)錯(cuò)誤的接口約束,就沒法對(duì)自定義錯(cuò)誤類型使用Go對(duì)error提供的其他功能了,比如說后面要介紹的錯(cuò)誤包裝。
所以針對(duì)自定義的錯(cuò)誤類型,我們也要讓他變成一個(gè)真正的Go error,方法就是讓它實(shí)現(xiàn)error接口定義的方法。
包裝錯(cuò)誤
在現(xiàn)實(shí)的程序應(yīng)用里,一個(gè)邏輯往往要經(jīng)多多層函數(shù)的調(diào)用才能完成,那在程序里我們的建議Error Handling 盡量留給上層的調(diào)用函數(shù)做,中間和底層的函數(shù)通過錯(cuò)誤包裝把自己要記的錯(cuò)誤信息附加再原始錯(cuò)誤上再返回給外層函數(shù)。
比如像下面這樣:
這段代碼從打印錯(cuò)誤信息的輸出上看沒什么問題,但是深層次的問題很明顯,我們丟失了原來的err?,因?yàn)樗呀?jīng)被我們的fmt.Errorf函數(shù)轉(zhuǎn)成一個(gè)新的字符串了。
基于這個(gè)背景,很多開源三方庫提供了錯(cuò)誤包裝、追加錯(cuò)誤調(diào)用棧等功能,用的最多的就是"github.com/pkg/errors"這個(gè)庫,提供了下面幾個(gè)主要的包裝錯(cuò)誤的功能。
Go官方在2019年發(fā)布1.13?版本,自己也增加了對(duì)錯(cuò)誤包裝的支持,不過并沒有提供什么Wrap?函數(shù),而是擴(kuò)展了fmt.Errorf?函數(shù),加了一個(gè)%w來生成一個(gè)包裝錯(cuò)誤。
Go1.13?引入了包裝錯(cuò)誤后,同時(shí)為內(nèi)置的errors?包添加了3個(gè)函數(shù),分別是Unwrap、Is和As。
先來聊聊Unwrap,顧名思義,它的功能就是為了獲取到包裝錯(cuò)誤里那個(gè)被嵌套的error。
這里需要注意的是,嵌套可以有很多層,我們調(diào)用一次errors.Unwrap?函數(shù)只能返回往里一層的error?,如果想獲取更里面的,需要調(diào)用多次errors.Unwrap?函數(shù)。最終如果一個(gè)error?不是warpping error,那么返回的是nil。
如果想得到最原始的error,建議自己封裝個(gè)工具函數(shù),類似這樣
對(duì)于我們文章開頭定義的那個(gè)自定義錯(cuò)誤MyError?想要把它變成可包裝的Error的話,還需要實(shí)現(xiàn)一個(gè)Unwrap()方法。
有了包裝錯(cuò)誤后,像具體某種錯(cuò)誤的判斷和錯(cuò)誤的類型轉(zhuǎn)換也得需要跟進(jìn)改一下才行。這就是errors?包在1.13?后新增的另外兩個(gè)工具函數(shù)Is和As的作用。接下來我們一個(gè)個(gè)來說。
errors.Is
在Go 1.13之前沒有包裝錯(cuò)誤的時(shí)候,程序里要判斷是不是同一個(gè)error可以直接簡單粗暴的:
這樣我們就可以通過判斷來做一些事情。但是現(xiàn)在有了包裝錯(cuò)誤后這樣辦法就不完美的,因?yàn)槟愀静恢婪祷氐倪@個(gè)err?是不是一個(gè)嵌套的error,嵌套了幾層。所以基于這種情況,Go為我們提供了errors.Is函數(shù)。
如果err?和目標(biāo)錯(cuò)誤target?是同一個(gè),那么返回true。
如果err? 是一個(gè)包裝錯(cuò)誤,目標(biāo)錯(cuò)誤target?也包含在這個(gè)嵌套錯(cuò)誤鏈中的話,那么也返回true。
下面是一個(gè)使用errors.Is判斷是否是同一錯(cuò)誤的例子。
errors.As
同樣在沒有包裝錯(cuò)誤前,我們要把error 轉(zhuǎn)換為一個(gè)具體類型的error,一般都是使用類型斷言或者 type switch,其實(shí)也就是類型斷言。
但是有了包裝錯(cuò)誤之后,返回的err可能是已經(jīng)被嵌套了,這種方式就不能用了,所以Go為我們在errors?包里提供了As函數(shù)。
As? 函數(shù)所做的就是遍歷錯(cuò)誤的嵌套鏈,從里面找到類型符合的error,然后把這個(gè)error賦給target參數(shù),這樣我們在程序里就可以使用轉(zhuǎn)換后的target了,因?yàn)檫@里有賦值,所以target必須是一個(gè)指針,這個(gè)也算是Go內(nèi)置包里的一個(gè)慣例了,像json.Unmarshal也是這樣。
所以把上面的例子用As 函數(shù)實(shí)現(xiàn)就變成了醬嬸:
總結(jié)
這篇文章主要是更新一下Error處理在Go 1.13以后新增的功能點(diǎn),以前的文章介紹的更多的還是使用"pkg/errors"那個(gè)包的方式,主要是前兩年以前公司用的Go版本一直是1.12,所以這部分知識(shí)我一直沒更新過來,這里簡單做個(gè)梳理。