Go Wiki:錯誤值:常見問題集

Go 2 錯誤值提案 為 Go 1.13 的 errorsfmt 套件新增功能。另外還有相容性套件 golang.org/x/xerrors,適用於較早版本的 Go。

我們建議使用 xerrors 套件以維持向後相容性。當您不再需要支援 1.13 之前的 Go 版本時,請使用對應的標準函式庫函式。此常見問題集使用 Go 1.13 的 errorsfmt 套件。

我該如何變更我的錯誤處理程式碼,以使用新功能?

您需要做好準備,您取得的錯誤可能會被封裝。

我已經使用 fmt.Errorf 搭配 %v%s 來提供錯誤內容。我什麼時候應該切換到 %w

常見的程式碼如下

if err := frob(thing); err != nil {
    return fmt.Errorf("while frobbing: %v", err)
}

使用新的錯誤功能,該程式碼會繼續像以前一樣運作,建構一個包含 err 文字的字串。從 %v 變更為 %w 不會變更該字串,但它會封裝 err,讓呼叫者可以使用 errors.Unwraperrors.Iserrors.As 存取它。

因此,如果您想要讓呼叫者看到底層錯誤,請使用 %w。請記住,這麼做可能會暴露實作細節,進而限制您的程式碼演進。呼叫者可以依賴您封裝的錯誤的型別和值,因此變更該錯誤現在可能會中斷它們。例如,如果套件 pkgAccessDatabase 函式使用 Go 的 database/sql 套件,則它可能會遇到 sql.ErrTxDone 錯誤。如果您使用 fmt.Errorf("accessing DB: %v", err) 傳回該錯誤,則呼叫者將看不到 sql.ErrTxtDone 是您傳回錯誤的一部分。但如果您改用 fmt.Errorf("accessing DB: %w", err) 傳回,則呼叫者可以合理地撰寫

err := pkg.AccessDatabase(...)
if errors.Is(err, sql.ErrTxDone) ...

在那個時間點,如果您不想要中斷您的客戶端,您必須始終傳回 sql.ErrTxDone,即使您切換到不同的資料庫套件也是如此。

如何新增內容到我已回傳的錯誤中,而不會中斷客戶端?

假設你的程式碼現在看起來像

return err

而你決定要在回傳 `err` 之前新增更多資訊。如果你撰寫

return fmt.Errorf("more info: %v", err)

你可能會中斷你的客戶端,因為 `err` 的身分已遺失;只有它的訊息仍然存在。

你可以改用 `%w` 包裝錯誤,撰寫

return fmt.Errorf("more info: %w", err)

這仍然會中斷使用 `==` 或型別斷言來測試錯誤的客戶端。但正如我們在這個常見問題的第一個問題中所討論的,錯誤的使用者應該轉移到 `errors.Is` 和 `errors.As` 函式。如果你能確定你的客戶端已這麼做,那麼從

return err

轉換到

return fmt.Errorf("more info: %w", err)

不會造成中斷變更。我正在撰寫沒有客戶端的程式碼。我應該包裝回傳的錯誤嗎?

由於你沒有客戶端,因此不受向後相容性的約束。但你仍然需要平衡兩個對立的考量

對於你回傳的每個錯誤,你必須權衡在協助你的客戶端和自我封閉之間的選擇。當然,這個選擇不只限於錯誤;作為套件作者,你會做出許多關於你的程式碼功能是否對客戶端來說很重要或只是實作細節的決定。

不過,對於錯誤,有一個中間選擇:你可以在不將錯誤本身公開給客戶端程式碼的情況下,將錯誤詳細資料公開給閱讀你的程式碼錯誤訊息的人員。執行此操作的一種方法是使用 `fmt.Errorf` 搭配 `%s` 或 `%v` 將詳細資料放入字串中。另一種方法是撰寫自訂錯誤類型,將詳細資料新增到其 `Error` 方法回傳的字串中,並避免定義 `Unwrap` 方法。

我維護一個套件,它會匯出一個錯誤檢查謂詞函式。我應該如何適應新功能?

你的套件有一個函式或方法 IsX(error) bool,它會回報一個錯誤是否具備某個屬性。一個自然的想法是修改 IsX 來解開它所傳遞的錯誤,檢查已封裝錯誤鏈中的每個錯誤的屬性。我們建議不要這麼做:行為的改變可能會損壞你的使用者。

你的情況就像標準的 os 套件,它有幾個這樣的函式。我們建議採用我們在那裡採取的方法。os 套件有幾個謂詞,但我們對它們大多數的處理方式相同。為了具體說明,我們將探討 os.IsExist

我們沒有改變 os.IsExist,而是讓 errors.Is(err, os.ErrExist) 以類似的方式運作,但 Is 會解開。 (我們透過讓 syscall.Errno 實作一個 Is 方法來做到這一點,如 errors.Is 文件中所述。)使用 errors.Is 始終都能正確運作,因為它只會存在於 Go 版本 1.13 及更新版本中。對於舊版本的 Go,你應該自行遞迴解開錯誤,對每個底層錯誤呼叫 os.IsExist

此技術僅在你控制被封裝的錯誤時才有效,因此你可以為它們新增 Is 方法。在這種情況下,我們建議

如果你無法控制所有可能具有屬性 X 的錯誤,你應該考慮新增另一個函式,它會在解開時測試屬性,例如

func IsXUnwrap(err error) bool {
    for e := err; e != nil; e = errors.Unwrap(e) {
        if IsX(e) {
            return true
        }
    }
    return false
}

或者你可以維持現狀,讓你的使用者自行解開。無論如何,你都應該改變 IsX 的文件說明,以澄清它不會解開。

我有一個實作 error 並包含巢狀錯誤的類型。我應該如何將它調整到新功能?

如果你的類型已經公開錯誤,請撰寫一個 Unwrap 方法。

例如,你的類型或許看起來像

type MyError struct {
    Err error
    // other fields
}

func (e *MyError) Error() string { return ... }

然後你應該新增

func (e *MyError) Unwrap() error { return e.Err }

你的類型將會與 errorsxerrorsIsAs 函式正確運作。

我們已經為標準函式庫中的 os.PathError 和其他類似類型這樣做了。

很明顯,如果巢狀錯誤是公開的,或是以其他方式對封包外的程式碼可見(例如透過類似 Unwrap 的方法),撰寫一個 Unwrap 方法是正確的選擇。但是,如果巢狀錯誤未公開給外部程式碼,你可能應該保持這種方式。透過從 Unwrap 傳回錯誤來使錯誤可見,將會讓你的客戶端依賴巢狀錯誤的類型,這可能會公開實作細節並限制封包的演進。請參閱上面對 %w 的討論以了解更多資訊。


此內容是 Go Wiki 的一部分。