Go Wiki:錯誤值:常見問題解答
Go 2 錯誤值提案 為 Go 1.13 標準庫的 errors 和 fmt 包添加了功能。還有一個相容性包 golang.org/x/xerrors,用於早期 Go 版本。
我們建議使用 xerrors 包以獲得向後相容性。當您不再希望支援 1.13 之前的 Go 版本時,請使用相應的標準庫函式。本 FAQ 使用 Go 1.13 中的 errors 和 fmt 包。
我應該如何修改我的錯誤處理程式碼以適應新功能?
您需要做好準備,您可能獲得的錯誤是包裝過的。
-
如果您當前使用
==比較錯誤,請改用errors.Is。示例if err == io.ErrUnexpectedEOF變為
if errors.Is(err, io.ErrUnexpectedEOF)- 形式為
if err != nil的檢查無需更改。 - 與
io.EOF的比較無需更改,因為io.EOF永遠不應該被包裝。
- 形式為
-
如果您使用型別斷言或型別開關檢查錯誤型別,請改用
errors.As。示例if e, ok := err.(*os.PathError); ok變為
var e *os.PathError if errors.As(err, &e)- 使用此模式還可以檢查錯誤是否實現了某個介面。(這是少數幾種介面指標合適的情況之一。)
- 將型別開關重寫為一系列 if-else。
我已經在用 fmt.Errorf 配合 %v 或 %s 為錯誤提供上下文。我什麼時候應該切換到 %w?
通常會看到類似這樣的程式碼:
if err := frob(thing); err != nil {
return fmt.Errorf("while frobbing: %v", err)
}
有了新的錯誤功能,這些程式碼將與以前完全一樣工作,生成一個包含 err 文字的字串。從 %v 更改為 %w 不會改變該字串,但它會包裝 err,允許呼叫者使用 errors.Unwrap、errors.Is 或 errors.As 來訪問它。
所以,如果您想向呼叫者公開底層錯誤,請使用 %w。請記住,這樣做可能會暴露實現細節,從而限制您程式碼的演進。呼叫者可以依賴您正在包裝的錯誤的型別和值,因此更改該錯誤現在可能會破壞他們。例如,如果您的包 pkg 的 AccessDatabase 函式使用 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)
這仍然會破壞使用 == 或型別斷言來測試錯誤的客戶。但正如我們在本 FAQ 的第一個問題中所討論的,錯誤的消費者應該遷移到 errors.Is 和 errors.As 函式。如果您能確保您的客戶已經這樣做了,那麼從...切換不是一個破壞性更改:
return err
轉換為
return fmt.Errorf("more info: %w", err)
我正在編寫新程式碼,沒有客戶端。我應該包裝返回的錯誤還是不包裝?
由於您沒有客戶,您不受向後相容性的約束。但您仍然需要權衡兩個相互對立的考慮:
- 讓客戶端程式碼訪問底層錯誤可以幫助它做出決策,從而帶來更好的軟體。
- 您公開的每個錯誤都將成為您 API 的一部分:您的客戶可能會依賴它,因此您無法更改它。
對於您返回的每個錯誤,您都必須權衡幫助您的客戶和鎖定自己的選擇。當然,這個選擇不僅僅是錯誤;作為包的作者,您會做出許多決定,例如您的程式碼的某個功能對客戶來說是否重要,還是實現細節。
但是,對於錯誤,有一箇中間選擇:您可以將錯誤細節公開給閱讀您程式碼錯誤訊息的人,而無需將錯誤本身暴露給客戶端程式碼。一種方法是使用 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 方法。在這種情況下,我們建議:
- 不要更改您的
IsX(error) bool函式;但要更改其文件,以說明它不解包。 - 如果您還沒有一個,請新增一個型別實現
error的全域性變數,該變量表示您的函式測試的條件。var ErrX = errors.New("has property X") - 向
IsX返回 true 的型別新增一個Is方法。當其引數等於ErrX時,Is方法應返回 true。
如果您無法控制所有具有屬性 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 }
這樣,您的型別將與 errors 和 xerrors 的 Is 和 As 函式正確配合使用。
我們已經為標準庫中的 os.PathError 和其他類似型別這樣做了。
很明顯,如果巢狀錯誤是匯出的,或者透過 Unwrap 等方法對您的包外部程式碼可見,那麼編寫 Unwrap 方法是正確的選擇。但是,如果巢狀錯誤沒有暴露給外部程式碼,您最好保持原樣。透過從 Unwrap 返回錯誤來使錯誤可見,將使您的客戶能夠依賴巢狀錯誤的型別,這可能會暴露實現細節並限制您包的演進。有關更多資訊,請參見上面關於 %w 的討論。
此內容是 Go Wiki 的一部分。