在 Go 中,錯誤處理不是透過例外完成的。
相反的,錯誤是實作了內建 error介面的型別的一般_值_。
error介面非常精簡。
它只包含一個方法 Error(),用來將錯誤訊息以字串的形式回傳。
type error interface {
Error() string
}
每當你定義一個函式,而執行過程中可能發生需要傳達給呼叫端的錯誤時,就必須把 error 列為回傳型別之一。
如果函式有多個回傳值,依照慣例,error 一定是最後一個。
func DoSomething() (int, error) {
// ...
}
你不一定要自己實作 error 介面。
若要建立簡單的錯誤,你可以使用標準函式庫套件 errors中的 errors.New()函式。
你只需要傳入以字串表示的錯誤訊息,errors.New()就會負責建立一個包含你的訊息、並實作 error介面的值。
如果函式要回傳錯誤,良好的做法是為其他所有回傳參數回傳零值:
func DoSomething() (SomeStruct, int, error) {
// ...
return SomeStruct{}, 0, errors.New("failed to calculate result")
}
你不應該假設,當錯誤發生時,所有函式都會為其他回傳值回傳零值。 最好的做法是假設:一旦發生錯誤,使用任何其他回傳值都不安全。 唯一的例外是文件明確指出,在發生錯誤時其他回傳值仍有意義的情況。
如果你想在多個地方使用這樣的簡單錯誤,應該為這個錯誤宣告一個變數,而不是在行內使用 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
}
如果你呼叫的函式會回傳錯誤,通常會把錯誤值存入名為 err的變數。
在使用函式實際的結果之前,你需要先確認沒有發生錯誤。
為了避免讓程式碼的「happy path」形成巢狀結構,應該先處理錯誤的情況。
我們可以用 == 和 != 將錯誤與 nil 比較,當 err 不等於 nil 時,就知道發生了錯誤。
func processUserFile() error {
file, err := os.Open("./users.csv")
if err != nil {
return err
}
// do something with file
}
大多數情況下,錯誤會如上面的範例所示,沿著函式堆疊往上回傳。 另一種處理錯誤的方式,是將它記錄下來,然後繼續進行其他操作。 良好的做法是要嘛回傳錯誤、要嘛記錄錯誤,絕對不要兩者都做。
由於 Go 中大多數函式都會把錯誤列為回傳值之一,你會在 Go 程式碼中到處看到/使用 if err != nil這種模式。
如果你希望錯誤包含比錯誤訊息字串更多資訊,可以建立自訂的錯誤型別。
如前所述,在 Go 中,任何實作 error介面的東西(也就是具有 Error() string方法的型別)都可以當作錯誤使用。
通常會使用結構體來建立自訂的錯誤型別。
依照慣例,自訂錯誤型別的名稱應該以 Error 結尾。
此外,最好使用指標接收器來定義 Error() string方法,若要了解背後的原因,請參閱這則 Stackoverflow 留言。
請注意,這表示你需要回傳自訂錯誤的指標,否則它不會被視為 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: "...",
}
}
你期盼已久的日子終於到來,現在你是阿爾卑斯山上美麗農場的驕傲主人。
你依然不喜歡一大早起床餵牛。
因為你是個優秀的工程師,所以你打造了一台餵食器,叫做FEED-M-ALL。
完成這個專案的最後一件事,就是寫出一段程式碼,計算每頭牛應該分到多少飼料。 每頭牛分到的量必須相同,你得避免衝突。 牛可是很敏感的。
幸運的是,你不需要自己推導出所有計算飼料份量的公式。 你用了一個在網路上找到的神祕外部函式庫。 據說它能讓牛群過得最開心。 這個函式庫提供了一個型別,實作了以下介面。 你在自己寫的程式碼裡會用到它。
type FodderCalculator interface {
FodderAmount(int) (float64, error)
FatteningFactor() (float64, error)
}
在你寫程式的過程中,會逐步改善錯誤處理,讓它更強健,之後在日常農場生活中使用時也更容易除錯。
首先,你專注於寫出計算每頭牛飼料份量所需的程式碼。
實作一個函式 DivideFood,接受一個 FodderCalculator 和一個整數的牛隻數量作為引數。
在這項任務中,假設傳入的牛隻數量永遠大於零。
這個函式應該回傳每頭牛的食物份量,型別為 float64,若發生錯誤則回傳該錯誤。
要進行計算,你得先取得所有牛的飼料總量。
作法是呼叫 FodderAmount 方法並傳入牛隻數量。
此外,你還需要一個用來乘上這個總量的係數。
你可以透過呼叫 FatteningFactor 方法取得這個係數。
有了這兩個值和牛隻數量,你就能算出每頭牛的食物份量(型別為 float64)。
這就是 DivideFood 函式應該回傳的內容。
如果你呼叫的其中一個方法回傳錯誤,執行就應該停止,並將該錯誤原樣從 DivideFood 函式回傳。
// For this example, we assume FodderAmount returns 50
// and FatteningFactor returns 1.5.
DivideFood(fodderCalculator, 5)
// => 15 <nil>
// Now assuming FodderAmount returns an error with message "something went wrong".
DivideFood(fodderCalculator, 5)
// => 0 "something went wrong"
在做上面的第一項任務時,你發現自己用的外部函式庫品質不如預期。 例如,它無法妥善處理無效的輸入。 你想在自己的程式碼裡加入輸入值的檢查,來繞過這個限制。
寫一個函式 ValidateInputAndDivideFood,簽章與上面的 DivideFood 相同。
DivideFood 並回傳該呼叫的結果。"invalid number of cows" 的錯誤。ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>
ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"
在傳遞之前先檢查牛隻數量是個好做法,但你對這個不夠具體的錯誤訊息不太滿意。
你決定做得更好,建立一個名為 InvalidCowsError 的自訂錯誤型別。
這個自訂錯誤應該保存牛隻數量(int)和自訂訊息(string),而 Error 方法應該將資料序列化成以下格式:
{number of cows} cows are invalid: {custom message}
有了自訂錯誤之後,實作一個函式 ValidateNumberOfCows,接受牛隻數量(整數)並回傳一個錯誤(或 nil)。
InvalidCowsError,自訂訊息設為 "there are no negative cows"。InvalidCowsError,自訂訊息設為 "no cows don't need food"。nil,表示驗證成功。err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"
在辛苦建立這個驗證函式之後,你發現已經傍晚了,於是離開書桌去欣賞山巒上的夕陽。 你把實際將新的驗證函式加進程式碼這件事,留到改天再說。