讓 Go 的錯誤處理更加強大

Go 所提供的默認的 errors 包有很多的不足。編寫多層架構應用程序並使用 API 公開功能的時候,相比於單純的 string 類型的值,更需要具有上下文信息的錯誤處理。意識到這個缺點後,我開始實現一個更強大,更優雅的 error 包。這是一個逐漸演化的過程,隨着時間推移,我需要在這個包中引入更多的功能。

在此,我們會探討我們如何使用一個 CustomError 數據類型爲應用中帶來更多的價值,並且使錯誤處理更強大。

首先需要明白的是,如果實現了 Error() 方法,Go 允許使用任何用戶定義的數據類型替代內置的 error 數據類型。

換言之,只要我們自定義的數據類型實現了返回一個 string 類型的 Error() 方法,我們就可以使用我們自己的數據類型代替 Go 默認提供的那個;任何應該返回 error 類型的函數都可以返回我們自定義的數據類型,並且運行如常。(譯者注:這裏所謂原生的 error “數據類型”,就是 error interface)

構建'CustomError' 類型

1、我們創建一個新的數據類型,該數據類型在應用程序中被解釋爲 error。我們將它命名爲 CustomError,首先,它會包含一個默認的 error 類型。這個 error 字段能夠讓我們在 CustomError 初始化的時候使用堆棧的跟蹤信息對其進行註釋(更多相關細節請看這裏 [1])。記錄這些堆棧信息可以讓我們在諸如 NewRelic APM 之類的平臺上更容易地調試錯誤。

2、Go 內置的 error 類型將錯誤當做 string 類型的值。我一直認爲這種方式是不對的,至少這樣是不足的。一個錯誤應當有與之關聯的類型——比如錯誤是否由於 SQL DB 寫入 / 更新 / 獲取操作失敗而導致的?或者錯誤是否是由於請求中沒有提供足夠的數據導致的?

現在,更加深入地瞭解下 ErrorCode 類型的實際情況。

我們依據可能在應用中捕獲到的每個錯誤類型創建多個 ErrorCode 常量。這樣一個 error 的解釋就基於 ErrorCode 而不是字符串了。

獲知一個 error 的類型可以讓我們以不同的方式處理不同的錯誤,也可以基於 error 的不同類型採取業務層面的決策。不僅僅只是業務決策,快速瀏覽下 ErrorCode 甚至可以指出系統出故障的精確範圍。

舉例來說,我在系統中的主要 RPC 中使用了大量的 5xx 錯誤在不同時間段的計數 (5xx Count vs Time)。通過瀏覽這個圖可以有助於定位系統錯誤的準確範圍。

3、出於提高可讀性的目標,我們同時也希望保留 error 的 string 解釋,這樣也能讓我們一眼就瞭解哪裏出了問題。

ErrorCode 告訴你一個錯誤的類型,但是不會告訴你它發生在代碼中的哪個位置。ErrorMsg 則服務於這個目的,並保持現有功能不變。

這裏可能有爭議,“爲什麼不使用在第 1 步中引入的 error 字段的錯誤信息?爲什麼要新加一個字段?”——這是因爲我們會使用 ErrorCodeErrorMsg 的組合來作爲一個 error (代碼請參考這裏 [2])。

但是僅有這兩個就足夠了嗎?如果我們想捕獲更多信息——比如導致一個 error 的上下文數據呢?

比方說,一個顧客打開一個查詢周圍餐廳的頁面,但是當他打開應用的時候,卻沒有給他展示任何東西。你也許需要捕獲更多上下文信息,比如 緯度經度 和其他一系列信息,方便你用來判斷是該區域真的沒有餐廳,還是一個維護性上的問題。

LoggingParams 正是解決辦法!

4、LoggingParams 允許你捕獲各種依賴於上下文的參數。

比如,我將這些在嚴重錯誤中產生的參數追加在告警信息中,看一下這些參數,有時就可以找出錯誤的根本原因。

如果沒有找到原因,它也有助於過濾出特定事件的錯誤日誌。

這是我們記錄 CustomError 時 Kibana 日誌看起來的樣子

5、最後,我們需要一些類似於 Go 中默認 error 類型申明 error != nil 之類的東西。

我們引入了一個名爲 existsbool 字段,當 CustomError 被創建時,該字段會被初始化爲 true

6、對於小部分錯誤,我們希望向最終用戶展示特定信息。這些信息便於他們理解並做出反應。

我們同樣維護了一個 ErrorCode 到用戶界面友好(UI-friendly)的信息的映射。

使用:

看上面的例子,你會意識到每次 Acquire() 被調用,調用函數會期望返回 CustomError 的錯誤類型,與此同時,這會給我們帶來全面提升。

譯者注:站在 error 使用角度。上圖中的做法可能並不是很好。Acquire() 函數返回的不是 error 而是作者自定義的 CustomError 結構體,等同於要與這個 CustomError 耦合在一起。如果 Acquire() 函數仍返回 error,上層通過 errors.As 解析出 CustomError 可能會更好。另外,error 實際上是 interface。將一個 struct 類型或者 struct 的指針賦值給 interface 的變量,則必定不爲空。執行下面的語句,err 必定不等於 nil。

var err error
err = Acquire()

if err != nil {
 ......
}

結論

我們已經看到了 CustomError 可以被用來使得 error 在多層次應用中更有解釋力。您可以在這裏 [3] 查看具有正確接口定義和實現的完整代碼。


via: https://medium.com/codealchemist/error-handling-in-go-made-more-powerful-ce164c2384ee

作者:Abhinav Bhardwaj[4] 譯者:dust347[5] 校對:polaris1119[6]

本文由 GCTT[7] 原創編譯,Go 中文網 [8] 榮譽推出,發佈在 Go 語言中文網公衆號,轉載請聯繫我們授權。

參考資料

[1]

這裏: https://github.com/abhinav-codealchemist/custom-error-go/blob/ca21f0e42b4ed57b5390491fe25fcb16eec7cffa/CustomError.go#L23

[2]

這裏: https://github.com/abhinav-codealchemist/custom-error-go/blob/ca21f0e42b4ed57b5390491fe25fcb16eec7cffa/CustomError.go#L22

[3]

這裏: https://github.com/abhinav-codealchemist/custom-error-go

[4]

Abhinav Bhardwaj: https://codealchemist.medium.com/

[5]

dust347: https://github.com/dust347

[6]

polaris1119: https://github.com/polaris1119

[7]

GCTT: https://github.com/studygolang/GCTT

[8]

Go 中文網: https://studygolang.com/

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LTZXAPvTHRTUcNH_v7ZSIg