在 Go 中,錯誤處理不是透過例外來完成的。
相對地,錯誤是實作了內建error介面的型別所具有的普通_值_。
error介面非常精簡。
它只包含一個方法Error(),用來以字串的形式回傳錯誤訊息。
type error interface {
Error() string
}
每當你定義的函式在執行過程中可能發生錯誤,而這個錯誤需要傳達給呼叫端時,就必須把error列為其中一個回傳型別。
如果函式有多個回傳值,依照慣例,error永遠是最後一個。
func DoSomething() (int, error) {
// ...
}
你不一定要自己實作錯誤介面。
若要建立簡單的錯誤,你可以使用errors.New()函式,它屬於標準函式庫的errors套件。
你唯一需要傳入的是以字串表示的錯誤訊息,errors.New()就會負責建立一個包含這則訊息、並實作error介面的值。
如果函式會回傳錯誤,好的做法是為其他所有回傳參數回傳零值:
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
你不應該假設,只要發生錯誤,所有函式都會為其他回傳值回傳零值。 最好的做法是假設:一旦回傳了錯誤,使用其他任何回傳值都不安全。
舉個例子,想像有個函式試著從檔案讀取資料,並回傳一個只填了一半的位元組切片和一個錯誤。 如果你在程式碼中重複使用那個回傳的位元組切片,並假設它因為發生錯誤而一定是空的,就可能碰上難以察覺的 bug。
唯一的例外是函式文件明確指出其他回傳值在發生錯誤時仍有意義的情況。
可以參考 [buffer 的 Write 說明文件][buffer-write] 這個例子。
請確保你提供的錯誤訊息盡可能具體,因為錯誤預設不包含任何堆疊追蹤。 依照慣例,錯誤訊息應該以小寫字母開頭,並且不以句點結尾。
如果你想在多個地方使用這樣的錯誤(或是想讓套件的使用者也能取用這個錯誤),就應該為這個錯誤宣告一個變數,而不是在行內使用errors.New。
依照慣例,這個變數的名稱應該以Err或err開頭(取決於它是否為匯出的)。
這類錯誤變數通常稱為_哨兵錯誤_。
import "errors"
var ErrNotFound = errors.New("resource was not found")
func DoSomething() error {
// ...
return ErrNotFound
}
若在函式執行期間沒有發生任何錯誤,就為錯誤回傳nil來表示:
func Foo() (int, error) {
return 10, nil
}
如果你需要用一些變數建構更複雜的錯誤訊息,可以利用字串格式化。
Go 有個專門用於此目的的fmt.Errorf函式。
它會建構錯誤訊息字串並回傳錯誤。
input := 123
action := "UPDATE"
err := fmt.Errorf("invalid input %d for action %s", input, action)
err.Error()
// => "invalid input 123 for action UPDATE"
如果你呼叫的函式會回傳錯誤,常見的做法是把錯誤值存到名為err的變數中。
在使用函式實際的結果之前,你需要先確認沒有發生錯誤。
我們可以用==和!=把錯誤與nil做比較,當err不是nil時,就知道發生了錯誤。
為了避免把程式碼的「正常路徑」寫成巢狀,應該先處理錯誤情況,通常是透過提前回傳。 想了解更多細節,請參考 Mat Ryer 的這篇著名文章。
// Do this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
// Not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err != nil {
// handle err
} else {
// do something with file
}
}
// Also not this:
func myFunc() error {
file, err := os.Open("./users.csv")
if err == nil {
// do something with file
}
// handle err
}
大多數情況下,錯誤會如上方的第一個範例所示,沿著函式堆疊往上回傳。 另一種處理錯誤的方式是把它記錄下來,然後繼續執行其他操作。 好的做法是要嘛回傳錯誤,要嘛記錄錯誤,但絕不要兩者都做。
由於 Go 中大多數函式都把錯誤列為其中一個回傳值,你會在 Go 程式碼中到處看到或用到if err != nil這個模式。
如果你希望錯誤包含比錯誤訊息字串更多的資訊,可以建立自訂的錯誤型別。
如前所述,在 Go 中,任何實作了error介面的東西(也就是具有Error() string方法)都可以當作錯誤使用。
通常會用結構體來建立自訂的錯誤型別。
依照慣例,自訂錯誤型別的名稱應該以Error結尾。
此外,最好使用指標接收者來定義Error() string方法,關於背後的原因,可以參考這則 Stack Overflow 留言。
請注意,這表示你需要回傳自訂錯誤的指標,否則它不會被視為error,因為非指標的值並不提供Error() string方法。
type MyCustomError struct {
message string
details string
}
func (e *MyCustomError) Error() string {
return fmt.Sprintf("%s, details: %s", e.message, e.details)
}
func someFunction() error {
// ...
return &MyCustomError{
message: "...",
details: "...",
}
}