農場

農場

學習練習

簡介

error 介面

在 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")
}
Caution

你不應該假設,當錯誤發生時,所有函式都會為其他回傳值回傳零值。 最好的做法是假設:一旦發生錯誤,使用任何其他回傳值都不安全。 唯一的例外是文件明確指出,在發生錯誤時其他回傳值仍有意義的情況。

如果你想在多個地方使用這樣的簡單錯誤,應該為這個錯誤宣告一個變數,而不是在行內使用 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)
}

在你寫程式的過程中,會逐步改善錯誤處理,讓它更強健,之後在日常農場生活中使用時也更容易除錯。

1. 平均分配食物

首先,你專注於寫出計算每頭牛飼料份量所需的程式碼。

實作一個函式 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"

2. 檢查牛隻數量

在做上面的第一項任務時,你發現自己用的外部函式庫品質不如預期。 例如,它無法妥善處理無效的輸入。 你想在自己的程式碼裡加入輸入值的檢查,來繞過這個限制。

寫一個函式 ValidateInputAndDivideFood,簽章與上面的 DivideFood 相同。

  • 如果傳入的牛隻數量大於 0,函式應該呼叫 DivideFood 並回傳該呼叫的結果。
  • 如果牛隻數量是 0 或更少,函式應該回傳一則訊息為 "invalid number of cows" 的錯誤。
ValidateInputAndDivideFood(fodderCalculator, 5)
// => 15 <nil>

ValidateInputAndDivideFood(fodderCalculator, -2)
// => 0 "invalid number of cows"

3. 改善錯誤處理

在傳遞之前先檢查牛隻數量是個好做法,但你對這個不夠具體的錯誤訊息不太滿意。 你決定做得更好,建立一個名為 InvalidCowsError 的自訂錯誤型別。

這個自訂錯誤應該保存牛隻數量(int)和自訂訊息(string),而 Error 方法應該將資料序列化成以下格式:

{number of cows} cows are invalid: {custom message}

有了自訂錯誤之後,實作一個函式 ValidateNumberOfCows,接受牛隻數量(整數)並回傳一個錯誤(或 nil)。

  • 如果牛隻數量小於 0,函式回傳一個 InvalidCowsError,自訂訊息設為 "there are no negative cows"。
  • 如果牛隻數量是 0,函式回傳一個 InvalidCowsError,自訂訊息設為 "no cows don't need food"。
  • 否則,函式回傳 nil,表示驗證成功。
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

在辛苦建立這個驗證函式之後,你發現已經傍晚了,於是離開書桌去欣賞山巒上的夕陽。 你把實際將新的驗證函式加進程式碼這件事,留到改天再說。

透過 GitHub 編輯 連結會在新視窗或分頁中開啟
Go Exercism

準備好開始 農場 了嗎?

註冊 Exercism,透過 34 個概念166 個練習 和真人引導來學習並精通 Go,全部免費。