預計將于 2023 年 2 月發(fā)布的 Go 1.20 有一個小的變化,對于那些大量使用錯誤包裝的應(yīng)用程序來說,可能會有效改進它們的錯誤處理方法。

讓我們看一下它的用法,但首先,需要簡要回顧一下什么是錯誤包裝。如果你已經(jīng)掌握了可以直接跳到下面的 “Go 1.20 新功能” 部分以獲取新的信息。


(資料圖)

Go 中的錯誤是實現(xiàn)一個非常簡單的接口:

typeerrorinterface{Error()string}

錯誤類型可以是任何東西,從string本身到int,但通常它們是struct類型。下面這個例子來自標準庫:

typeerrstruct{sstring}func(e*err)Error()string{returne.s}

要檢查 Go 中的錯誤,你只需比較一個值(在本例中為int值):

iferr==io.EOF{//...}

第二種常見的用法是檢查錯誤的類型,那也意味著要寫更多的代碼:

ifnerr,ok:=err.(net.Error){//...(usenerrwhichisanet.Error)}

在上面的例子中,類型斷言測試類型net.Error的err值,并創(chuàng)建一個新變量nerr,它可以在 if 語句中使用。Go 中的錯誤方便理解、易于使用且非常高效。

錯誤包裝

從 Go 1.13 開始,引入了錯誤包裝。包裝允許將錯誤嵌入到其他錯誤中,就像在其他語言中包裝異常一樣。這非常實用,比如函數(shù)遇到 “record not found” 錯誤時,可以向錯誤信息中添加更多上下文信息,例如 “unknown user: record not found”。

Go 中錯誤包裝設(shè)計背后的有趣想法是:契約不用關(guān)心錯誤類型、結(jié)構(gòu)或它們是如何創(chuàng)建的。而唯一關(guān)注的是解包過程和轉(zhuǎn)換為字符串,因為這兩者是必須的。這就非常容易實現(xiàn):支持解包的錯誤類型必須實現(xiàn)Unwrap() error方法。

標準庫中沒有(命名的)接口可以向您展示,因為接口是隱式實現(xiàn)的,沒有必要單獨寫一個。這里我們寫一個只是為了更好說明這篇文章:

typeWrappedErrorinterface{Unwrap()error}

我們來看看 Go 標準庫(實際上是 package fmt)中是如何實現(xiàn)包裝錯誤的:

typewrapErrorstruct{msgstringerrerror}func(e*wrapError)Error()string{returne.msg}func(e*wrapError)Unwrap()error{returne.err}

由于上面錯誤類型實現(xiàn)了Error() string方法,所以說 Go 中的錯誤實際上最終是字符串并沒有錯,因此需要一種創(chuàng)建這些字符串的良好機制。這就是標準庫中的函數(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())

一個特殊格式的動詞%w,每次調(diào)用只能使用一次(稍后會詳細介紹),用于錯誤參數(shù)。除此之外,該函數(shù)的工作方式類似于fmt.Printf函數(shù)。下面的例子打印了這個結(jié)果:

unknownuser"lzap"(id13):notfound

如你所見,錯誤包裝本質(zhì)上是一個鏈表。要解包錯誤,請使用errors.Unwrap函數(shù),該函數(shù)將為鏈表中的最后一個錯誤值返回nil。要檢查錯誤類型或值,需要遍歷整個列表,這對于需要進行頻繁的錯誤檢查不太實用。幸運的是,有兩個輔助函數(shù)可以做到這一點。

檢查包裝錯誤列表中的值:

iferrors.Is(err,RecordNotFoundErr){//...}

檢查特定類型(下面例子是來自標準庫的網(wǎng)絡(luò)錯誤):

varnerr*net.Erroriferrors.As(err,&nerr){//...(usenerrwhichisa*net.Error)}

以上總結(jié)了 Go 1.13 及更高版本中的錯誤包裝。

Go 1.20 新特性

讓我們看看 Go 1.20 中真正的新功能,從函數(shù)errors.Join開始,它通過可變參數(shù)包裝錯誤列表:

err1:=errors.New("err1")err2:=errors.New("err2")err:=errors.Join(err1,err2)fmt.Println(err)

當事先不知道錯誤數(shù)量時,此功能可用于將錯誤連接在一起。一個很好的例子是從 goroutines 收集錯誤。值得一提的是,該函數(shù)將列表中的錯誤與換行符連接起來。上面的代碼片段打印:

err1err2

對于許多應(yīng)用程序或(日志記錄)庫來說,這可能會存在問題,它們期望錯誤通常只是沒有換行符的字符串。幸運的是,Go 1.20 中的另一個變化改變了fmt.Errorf的行為:該函數(shù)現(xiàn)在接受多個%w格式說明符:

err1:=errors.New("err1")err2:=errors.New("err2")err:=fmt.Errorf("%w+%w",err1,err2)fmt.Println(err)

以前會導致格式錯誤的格式字符串現(xiàn)在可以正確打?。?/p>

err1+err2

同時包裝多個錯誤實現(xiàn)Unwrap() error,這是可能的嗎?

事實證明,在 Go 1.20 標準庫中有一種新的機制: 實現(xiàn)Unwrap() []error函數(shù)的錯誤類型可以包裝多個錯誤。讓我們來看看這是如何在庫中實現(xiàn)的:

typejoinErrorstruct{errs[]error}func(e*joinError)Error()string{//concatenateerrorswithanewlinecharacter}func(e*joinError)Unwrap()[]error{returne.errs}

一個理論上的接口,但標準庫中實際不存在,如下所示:

typeMultiWrappedErrorinterface{Unwrap()[]error}

由于 Go 不允許方法重載,因此每種類型都可以實現(xiàn)Unwrap() error或Unwrap() []error,但不能同時實現(xiàn)。還記得我提到過包裝錯誤本質(zhì)上是一個鏈表嗎?實現(xiàn)前一個(新引入的)方法的類型實際上形成了一個鏈接樹,函數(shù)errors.Is和errors.As的工作方式相同,只是現(xiàn)在它們需要遍歷樹而不是列表。根據(jù)文檔,該實現(xiàn)執(zhí)行預排序、深度優(yōu)先遍歷。

這確實是 Go 1.20 帶來的全部,它可能看起來是一個小的變化,但它提供了如何有效和干凈地處理錯誤的新方法。在展示真實示例之前,讓我總結(jié)一下新功能:

新的Unwrap []error函數(shù)契約允許遍歷錯誤樹。

新的errors.Join函數(shù),這是一個方便的函數(shù),用于連接兩個錯誤字符串值(使用換行符)。

現(xiàn)有函數(shù)errors.Is和errors.As已更新,可以同時處理錯誤列表和錯誤樹。

現(xiàn)有函數(shù)fmt.Errorf現(xiàn)在接受多個%w格式動詞。實踐

上面這一切都很棒,但是你如何在實踐中利用它呢?

在一個小型 REST API 微服務(wù)中,我們通過errors.New和fmt.Errorf處理來自 DAO 包(數(shù)據(jù)庫)、REST 客戶端(其他后端服務(wù))和其他包的各種錯誤。返回的 HTTP 狀態(tài)代碼應(yīng)該是 2xx、4xx 或 5xx,具體取決于錯誤狀態(tài)以遵循最佳 REST API 實踐。實現(xiàn)此過程的一種方法是解開主 HTTP 處理程序中的錯誤并找出它是哪種錯誤。

然而,通過多重錯誤包裝,現(xiàn)在可以包裝根本原因(例如數(shù)據(jù)庫返回 “no records found” )和返回給用戶 HTTP 代碼(在本例中為 404)。

一個工作示例如下所示:

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)

從這樣的人工代碼片段中可能看起來不太明顯的是,實際上的錯誤聲明通常分布在許多包中,并且不容易跟蹤所有可能的錯誤以確保所需的 HTTP 狀態(tài)代碼。在這種方法中,所有在一個地方聲明的應(yīng)用程序級包裝錯誤也包含了 HTTP 代碼。

請注意,這在 Go 1.19 或更早版本中是不可能的,因為fmt.Errorf函數(shù)只會包裝第一個錯誤。該代碼確實在 1.19 上可以編譯,甚至不會產(chǎn)生運行時恐慌,但它實際上不會工作。

顯然,常見的 HTTP 狀態(tài)代碼很容易成為一種新的錯誤類型(基于int類型),因此可以通過errors.As輕松提取實際代碼,但我想讓示例保持簡單。

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 上自由運行上述代碼。確保使用 “dev branch” 或 Go 的 1.20+ 版本。現(xiàn)有應(yīng)用

在你的應(yīng)用程序中實施新功能時,請注意errors.Unwrap函數(shù)。對于具有Unwrap() []error的錯誤類型,它總是返回nil:

err1:=errors.New("err1")err2:=errors.New("err2")err:=errors.Join(err1,err2)unwrapped:=errors.Unwrap(err)fmt.Println(unwrapped)

由于 Go 1.X 兼容性承諾,這會打印出 “nil”。當你引入多個包裝錯誤時,請確保檢查展開代碼。幸運的是,典型 Go 代碼中的大部分錯誤檢查都是使用errors.Is和errors.As完成的。

錯誤包裝并不是 Go 中所有錯誤處理的最終解決方案。它只是提供了一種干凈的方法來處理典型 Go 應(yīng)用程序中的錯誤,對于簡單應(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

校對:watermelo

往期推薦

谷歌發(fā)布查找開源漏洞的Go工具OSV-Scanner

最好的Go框架:沒有框架?

「每周譯Go」如何在Go中構(gòu)造For 循環(huán)

想要了解Go更多內(nèi)容,歡迎掃描下方關(guān)注公眾號,回復關(guān)鍵詞 [實戰(zhàn)群],就有機會進群和我們進行交流

分享、在看與點贊Go

關(guān)鍵詞: