大家好,我是煎魚。
在 Go 的程式設計中,錯誤處理機制的處理永遠是大家在討論。不過 Go1 沒法大動干戈了,那就想辦法繼續優化吧。
今天煎魚給大家介紹一個五一假期期間學習時看到的一個新提案。
背景
在現階段,我們在標準庫中能夠包裝錯誤的唯一方法是使用 fmt.Errorf
,可以操作的空間是比較小的。
這意味著我們對錯誤所能做的就是將錯誤內容新增到其 .Error()
輸出中,以此宣告 error 型別的值。
如下程式碼:
err := fmt.Errorf("煎魚:%s", errors.New("放假中"))
if err != nil {}
但業務訴求往往沒那麼簡單。這個時候如果我們希望在收到錯誤資訊時,返回堆疊並提供其他資訊(例如:業務狀態碼)時,就沒有什麼特別簡單的方法。
硬要做,只有如下 3 種方案:
- 你可以返回一個新的其他錯誤,但會失去原始錯誤的上下文資訊。
- 你可以用
fmt.Errorf
來包裝錯誤,這只是增加了文字輸出,不是呼叫者可以通過程式設計檢查的東西。 - 你可以寫一個複雜的錯誤包裝結構,其中包含你想檢查的後設資料,以此使用
error.Is
、.As
和.Unwrap
來工作,以允許呼叫者訪問錯誤的根本原因。
現在最靠譜的是第 3 種方式,最完整,對應的是在 Go1.13 新增的 error 系列方法,還在青壯年階段(多年來唯一新增的錯誤處理補全)。
提案的原作者認為現階段還是不夠簡單方便。
新提案
新提案是希望在標準庫 errors 中實現一個更簡單的函式來達到上述第 3 點的效果,支援將任何錯誤與任何其他錯誤包裝在一起,從而使它們形成一個新的包裝錯誤列表。
如下程式碼:
// With returns an error that wraps err with other.
func With(err, other error) error
這個被包裹起來的錯誤類似於連結串列,可以複用 errors.Unwrap
來遍歷列表。而類連結串列儲存,就有先後順序的問題。
在 With
函式中,other
引數的錯誤將會放在包裝錯誤列表的頭部。如果在呼叫 With
函式時是 With(b->a, d->c)
,呈現在內的錯誤列表是:d->c->b->a。
對應的使用場景:
errors.Is(errors.With(err, other)):
- 判別標準:errors.Is(other) || errors.Is(err)。
errors.As(errors.With(err, other), target):
- 判別標準:errors.As(other, target) || errors.As(err, target)
errors.With(err, other).Error():
- 輸出結果是 other.Error() + ": " + err.Error()。
提案作者@Nate Finch 希望通過這種錯誤包裝方式,對既有的程式碼改動是最小的。也能提供最廣泛的功能適用性,認為是有價值的。
案例
場景
作者給出了一個非常經典的使用者案例。在我們平時寫應用程式碼時,在寫過的每個 go 應用程式中都看到了它。
應用中有一個返回特定域錯誤的包,例如返回 pq.ErrNoRows 的 postgres 驅動程式。
您希望將該錯誤向上傳遞到堆疊以維護原始錯誤的上下文,但您不希望呼叫者必須知道 postgres 錯誤才能知道如何從儲存層處理此錯誤。
改造
可以使用新的 With 函式,您可以通過眾所周知的錯誤型別新增後設資料,以便可以一致地檢查您的函式返回的錯誤,而不管底層實現如何。
如下程式碼:
// SetUserName sets the name of the user with the given id. This method returns
// flags.NotFound if the user isn't found or flags.Conflict if a user with that
// name already exists.
func (st *Storage) SetUserName(id uuid.UUID, name string) error {
err := st.db.SetUser(id, "name="+name)
if errors.Is(err, pq.ErrNoRows) {
return nil, errors.With(err, flags.NotFound)
}
var pqErr *pq.Error
if errors.As(err, &pqErr) && pqErr.Constraint == "unique_user_name" {
return errors.With(err, flags.Conflict)
}
if err != nil {
// some other unknown error
return fmt.Errorf("error setting name on user with id %v: %w", err)
}
return nil
}
這種錯誤通常稱為哨兵錯誤。
總結
今天給大家介紹的這個提案,還是比較貼合我們日常工作中的使用場景的。平時寫 Go 應用程式,思考的多,就會折騰這個問題。會出現,莫非要根據錯誤文字來判斷錯誤內容?
因此像是業內錯誤庫,或是之前看毛老師講的,都會進行相關的設計。這份提案也是一個不錯的補充了。
文章持續更新,可以微信搜【腦子進煎魚了】閱讀,本文 GitHub github.com/eddycjy/blog 已收錄,學習 Go 語言可以看 Go 學習地圖和路線,歡迎 Star 催更。
推薦閱讀
- Go 語言入門系列:初探 Go 專案實戰
- Go 語言程式設計之旅:深入用 Go 做專案
- Go 語言設計哲學:瞭解 Go 的為什麼和設計思考
- Go 語言進階之旅:進一步深入 Go 原始碼
- Go 探討了 13 年,怎麼解決再賦值的坑?
- 10+ 條 Go 官方諺語,你知道幾條?