預(yù)計(jì)將于 2023 年 2 月發(fā)布的 Go 1.20 有一個(gè)小的變化,對(duì)于那些大量使用錯(cuò)誤包裝的應(yīng)用程序來說,可能會(huì)有效改進(jìn)它們的錯(cuò)誤處理方法。
讓我們看一下它的用法,但首先,需要簡(jiǎn)要回顧一下什么是錯(cuò)誤包裝。如果你已經(jīng)掌握了可以直接跳到下面的 “Go 1.20 新功能” 部分以獲取新的信息。
(相關(guān)資料圖)
Go 中的錯(cuò)誤是實(shí)現(xiàn)一個(gè)非常簡(jiǎn)單的接口:
typeerrorinterface{Error()string}
錯(cuò)誤類型可以是任何東西,從string本身到int,但通常它們是struct類型。下面這個(gè)例子來自標(biāo)準(zhǔn)庫:
typeerrstruct{sstring}func(e*err)Error()string{returne.s}
要檢查 Go 中的錯(cuò)誤,你只需比較一個(gè)值(在本例中為int值):
iferr==io.EOF{//...}
第二種常見的用法是檢查錯(cuò)誤的類型,那也意味著要寫更多的代碼:
ifnerr,ok:=err.(net.Error){//...(usenerrwhichisanet.Error)}
在上面的例子中,類型斷言測(cè)試類型net.Error的err值,并創(chuàng)建一個(gè)新變量nerr,它可以在 if 語句中使用。Go 中的錯(cuò)誤方便理解、易于使用且非常高效。
錯(cuò)誤包裝從 Go 1.13 開始,引入了錯(cuò)誤包裝。包裝允許將錯(cuò)誤嵌入到其他錯(cuò)誤中,就像在其他語言中包裝異常一樣。這非常實(shí)用,比如函數(shù)遇到 “record not found” 錯(cuò)誤時(shí),可以向錯(cuò)誤信息中添加更多上下文信息,例如 “unknown user: record not found”。
Go 中錯(cuò)誤包裝設(shè)計(jì)背后的有趣想法是:契約不用關(guān)心錯(cuò)誤類型、結(jié)構(gòu)或它們是如何創(chuàng)建的。而唯一關(guān)注的是解包過程和轉(zhuǎn)換為字符串,因?yàn)檫@兩者是必須的。這就非常容易實(shí)現(xiàn):支持解包的錯(cuò)誤類型必須實(shí)現(xiàn)Unwrap() error方法。
標(biāo)準(zhǔn)庫中沒有(命名的)接口可以向您展示,因?yàn)榻涌谑请[式實(shí)現(xiàn)的,沒有必要單獨(dú)寫一個(gè)。這里我們寫一個(gè)只是為了更好說明這篇文章:
typeWrappedErrorinterface{Unwrap()error}
我們來看看 Go 標(biāo)準(zhǔn)庫(實(shí)際上是 package fmt)中是如何實(shí)現(xiàn)包裝錯(cuò)誤的:
typewrapErrorstruct{msgstringerrerror}func(e*wrapError)Error()string{returne.msg}func(e*wrapError)Unwrap()error{returne.err}
由于上面錯(cuò)誤類型實(shí)現(xiàn)了Error() string方法,所以說 Go 中的錯(cuò)誤實(shí)際上最終是字符串并沒有錯(cuò),因此需要一種創(chuàng)建這些字符串的良好機(jī)制。這就是標(biāo)準(zhǔn)庫中的函數(shù)fmt.Errorf發(fā)揮作用的地方:
varRecordNotFoundErr=errors.New("notfound")constname,id="lzap",13werr:=fmt.Errorf("unknownuser%q(id%d):%w",name,id,recordNotFoundErr)fmt.Println(werr.Error())
一個(gè)特殊格式的動(dòng)詞%w,每次調(diào)用只能使用一次(稍后會(huì)詳細(xì)介紹),用于錯(cuò)誤參數(shù)。除此之外,該函數(shù)的工作方式類似于fmt.Printf函數(shù)。下面的例子打印了這個(gè)結(jié)果:
unknownuser"lzap"(id13):notfound
如你所見,錯(cuò)誤包裝本質(zhì)上是一個(gè)鏈表。要解包錯(cuò)誤,請(qǐng)使用errors.Unwrap函數(shù),該函數(shù)將為鏈表中的最后一個(gè)錯(cuò)誤值返回nil。要檢查錯(cuò)誤類型或值,需要遍歷整個(gè)列表,這對(duì)于需要進(jìn)行頻繁的錯(cuò)誤檢查不太實(shí)用。幸運(yùn)的是,有兩個(gè)輔助函數(shù)可以做到這一點(diǎn)。
檢查包裝錯(cuò)誤列表中的值:
iferrors.Is(err,RecordNotFoundErr){//...}
檢查特定類型(下面例子是來自標(biāo)準(zhǔn)庫的網(wǎng)絡(luò)錯(cuò)誤):
varnerr*net.Erroriferrors.As(err,&nerr){//...(usenerrwhichisa*net.Error)}
以上總結(jié)了 Go 1.13 及更高版本中的錯(cuò)誤包裝。
Go 1.20 新特性讓我們看看 Go 1.20 中真正的新功能,從函數(shù)errors.Join開始,它通過可變參數(shù)包裝錯(cuò)誤列表:
err1:=errors.New("err1")err2:=errors.New("err2")err:=errors.Join(err1,err2)fmt.Println(err)
當(dāng)事先不知道錯(cuò)誤數(shù)量時(shí),此功能可用于將錯(cuò)誤連接在一起。一個(gè)很好的例子是從 goroutines 收集錯(cuò)誤。值得一提的是,該函數(shù)將列表中的錯(cuò)誤與換行符連接起來。上面的代碼片段打?。?/p>
err1err2
對(duì)于許多應(yīng)用程序或(日志記錄)庫來說,這可能會(huì)存在問題,它們期望錯(cuò)誤通常只是沒有換行符的字符串。幸運(yùn)的是,Go 1.20 中的另一個(gè)變化改變了fmt.Errorf的行為:該函數(shù)現(xiàn)在接受多個(gè)%w格式說明符:
err1:=errors.New("err1")err2:=errors.New("err2")err:=fmt.Errorf("%w+%w",err1,err2)fmt.Println(err)
以前會(huì)導(dǎo)致格式錯(cuò)誤的格式字符串現(xiàn)在可以正確打?。?/p>
err1+err2
同時(shí)包裝多個(gè)錯(cuò)誤實(shí)現(xiàn)Unwrap() error,這是可能的嗎?
事實(shí)證明,在 Go 1.20 標(biāo)準(zhǔn)庫中有一種新的機(jī)制: 實(shí)現(xiàn)Unwrap() []error函數(shù)的錯(cuò)誤類型可以包裝多個(gè)錯(cuò)誤。讓我們來看看這是如何在庫中實(shí)現(xiàn)的:
typejoinErrorstruct{errs[]error}func(e*joinError)Error()string{//concatenateerrorswithanewlinecharacter}func(e*joinError)Unwrap()[]error{returne.errs}
一個(gè)理論上的接口,但標(biāo)準(zhǔn)庫中實(shí)際不存在,如下所示:
typeMultiWrappedErrorinterface{Unwrap()[]error}
由于 Go 不允許方法重載,因此每種類型都可以實(shí)現(xiàn)Unwrap() error或Unwrap() []error,但不能同時(shí)實(shí)現(xiàn)。還記得我提到過包裝錯(cuò)誤本質(zhì)上是一個(gè)鏈表嗎?實(shí)現(xiàn)前一個(gè)(新引入的)方法的類型實(shí)際上形成了一個(gè)鏈接樹,函數(shù)errors.Is和errors.As的工作方式相同,只是現(xiàn)在它們需要遍歷樹而不是列表。根據(jù)文檔,該實(shí)現(xiàn)執(zhí)行預(yù)排序、深度優(yōu)先遍歷。
這確實(shí)是 Go 1.20 帶來的全部,它可能看起來是一個(gè)小的變化,但它提供了如何有效和干凈地處理錯(cuò)誤的新方法。在展示真實(shí)示例之前,讓我總結(jié)一下新功能:
新的Unwrap []error函數(shù)契約允許遍歷錯(cuò)誤樹。
新的errors.Join函數(shù),這是一個(gè)方便的函數(shù),用于連接兩個(gè)錯(cuò)誤字符串值(使用換行符)。
現(xiàn)有函數(shù)errors.Is和errors.As已更新,可以同時(shí)處理錯(cuò)誤列表和錯(cuò)誤樹。
現(xiàn)有函數(shù)fmt.Errorf現(xiàn)在接受多個(gè)%w格式動(dòng)詞。實(shí)踐上面這一切都很棒,但是你如何在實(shí)踐中利用它呢?
在一個(gè)小型 REST API 微服務(wù)中,我們通過errors.New和fmt.Errorf處理來自 DAO 包(數(shù)據(jù)庫)、REST 客戶端(其他后端服務(wù))和其他包的各種錯(cuò)誤。返回的 HTTP 狀態(tài)代碼應(yīng)該是 2xx、4xx 或 5xx,具體取決于錯(cuò)誤狀態(tài)以遵循最佳 REST API 實(shí)踐。實(shí)現(xiàn)此過程的一種方法是解開主 HTTP 處理程序中的錯(cuò)誤并找出它是哪種錯(cuò)誤。
然而,通過多重錯(cuò)誤包裝,現(xiàn)在可以包裝根本原因(例如數(shù)據(jù)庫返回 “no records found” )和返回給用戶 HTTP 代碼(在本例中為 404)。
一個(gè)工作示例如下所示:
packagemainimport("errors""fmt")//commonHTTPstatuscodesvarNotFoundHTTPCode=errors.New("404")varUnauthorizedHTTPCode=errors.New("401")//databaseerrorsvarRecordNotFoundErr=errors.New("DB:recordnotfound")varAffectedRecordsMismatchErr=errors.New("DB:affectedrecordsmismatch")//HTTPclienterrorsvarResourceNotFoundErr=errors.New("HTTPclient:resourcenotfound")varResourceUnauthorizedErr=errors.New("HTTPclient:unauthorized")//applicationerrors(thenewfeature)varUserNotFoundErr=fmt.Errorf("usernotfound:%w(%w)",RecordNotFoundErr,NotFoundHTTPCode)varOtherResourceUnauthorizedErr=fmt.Errorf("unauthorizedcall:%w(%w)",ResourceUnauthorizedErr,UnauthorizedHTTPCode)funchandleError(errerror){iferrors.Is(err,NotFoundHTTPCode){fmt.Println("Willreturn404")}elseiferrors.Is(err,UnauthorizedHTTPCode){fmt.Println("Willreturn401")}else{fmt.Println("Willreturn500")}fmt.Println(err.Error())}funcmain(){handleError(UserNotFoundErr)handleError(OtherResourceUnauthorizedErr)}
這將打印:
Willreturn404usernotfound:DB:recordnotfound(404)Willreturn401unauthorizedtocallotherservice:HTTPclient:unauthorized(401)
從這樣的人工代碼片段中可能看起來不太明顯的是,實(shí)際上的錯(cuò)誤聲明通常分布在許多包中,并且不容易跟蹤所有可能的錯(cuò)誤以確保所需的 HTTP 狀態(tài)代碼。在這種方法中,所有在一個(gè)地方聲明的應(yīng)用程序級(jí)包裝錯(cuò)誤也包含了 HTTP 代碼。
請(qǐng)注意,這在 Go 1.19 或更早版本中是不可能的,因?yàn)閒mt.Errorf函數(shù)只會(huì)包裝第一個(gè)錯(cuò)誤。該代碼確實(shí)在 1.19 上可以編譯,甚至不會(huì)產(chǎn)生運(yùn)行時(shí)恐慌,但它實(shí)際上不會(huì)工作。
顯然,常見的 HTTP 狀態(tài)代碼很容易成為一種新的錯(cuò)誤類型(基于int類型),因此可以通過errors.As輕松提取實(shí)際代碼,但我想讓示例保持簡(jiǎn)單。
Feel free to play around with the code on Go Playground. Make sure to use “dev branch” or 1.20+ version of Go. 可以在 Go Playground 上自由運(yùn)行上述代碼。確保使用 “dev branch” 或 Go 的 1.20+ 版本?,F(xiàn)有應(yīng)用在你的應(yīng)用程序中實(shí)施新功能時(shí),請(qǐng)注意errors.Unwrap函數(shù)。對(duì)于具有Unwrap() []error的錯(cuò)誤類型,它總是返回nil:
err1:=errors.New("err1")err2:=errors.New("err2")err:=errors.Join(err1,err2)unwrapped:=errors.Unwrap(err)fmt.Println(unwrapped)
由于 Go 1.X 兼容性承諾,這會(huì)打印出 “nil”。當(dāng)你引入多個(gè)包裝錯(cuò)誤時(shí),請(qǐng)確保檢查展開代碼。幸運(yùn)的是,典型 Go 代碼中的大部分錯(cuò)誤檢查都是使用errors.Is和errors.As完成的。
錯(cuò)誤包裝并不是 Go 中所有錯(cuò)誤處理的最終解決方案。它只是提供了一種干凈的方法來處理典型 Go 應(yīng)用程序中的錯(cuò)誤,對(duì)于簡(jiǎn)單應(yīng)用程序來說也許就完全足夠了。原文地址:https://lukas.zapletalovi.com/posts/2022/wrapping-multiple-errors/原文作者:Luká? Zapletal本文永久鏈接:https://github.com/gocn/translator/blob/master/2022/w50_Wrapping_multiple_errors譯者:haoheipi校對(duì):watermelo
往期推薦
谷歌發(fā)布查找開源漏洞的Go工具OSV-Scanner最好的Go框架:沒有框架?
「每周譯Go」如何在Go中構(gòu)造For 循環(huán)想要了解Go更多內(nèi)容,歡迎掃描下方關(guān)注公眾號(hào),回復(fù)關(guān)鍵詞 [實(shí)戰(zhàn)群],就有機(jī)會(huì)進(jìn)群和我們進(jìn)行交流
分享、在看與點(diǎn)贊Go
Copyright ? 2015-2022 華中旅游網(wǎng)版權(quán)所有 備案號(hào):京ICP備12018864號(hào)-26 聯(lián)系郵箱:2 913 236 @qq.com