騷爆了... Go 錯誤處理中再套個娃,能解決煩惱不?

煎魚發表於2022-05-26

大家好,我是煎魚。

在 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 催更。

推薦閱讀

參考

相關文章