農場

農場

学習演習

はじめに

errorインターフェース

Goでは、エラー処理は例外で行うのではありません。 そうではなく、エラーは、組み込みのerrorインターフェースを実装した型の、ごく普通の_値_です。 errorインターフェースはごく最小限で、エラーメッセージを文字列として返すError()メソッドを1つだけ持ちます。

type error interface {
  Error() string
}

実行中にエラーが起こり、それが呼び出し元まで届く必要がある関数を定義するときは、戻り値の型の1つにerrorを含める必要があります。 関数が複数の戻り値を持つ場合、慣習としてerrorは常に最後に置きます。

func DoSomething() (int, error) {
  // ...
}

シンプルなエラーの作成と返却

エラーインターフェースをいつも自分で実装する必要はありません。 シンプルなエラーを作るには、標準ライブラリのerrorsパッケージにあるerrors.New()関数を使えます。 渡す必要があるのは、エラーメッセージを文字列として1つだけです。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という変数に格納するのが一般的です。 関数の実際の結果を使う前に、エラーがなかったことを確認する必要があります。

コードの「ハッピーパス」が入れ子になるのを避けるために、エラーのケースを先に処理しましょう。 ==と!=を使ってエラーとnilを比較でき、errがnilでなければエラーがあったとわかります。

func processUserFile() error {
	file, err := os.Open("./users.csv")
	if err != nil {
		return err
	}

	// do something with file
}

ほとんどの場合、上の例のように、エラーは関数のスタックを上へたどって返されます。 別の対処法としては、エラーをログに記録し、そのまま別の処理を続けることもできます。 エラーは返すかログに記録するかのどちらかにするのが良い習慣で、両方はしません。

Goのほとんどの関数は戻り値の1つにエラーを含むため、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. 食べ物を均等に分ける

まず最初に、牛1頭あたりの飼料の量を計算するために必要なコードを書くことに集中します。

FodderCalculatorと、牛の頭数を表す整数を引数に取る関数DivideFoodを実装します。 このタスクでは、渡される牛の頭数は常に0より大きいものと仮定します。 この関数は、牛1頭あたりの餌の量をfloat64として返すか、エラーが発生した場合はそのエラーを返す必要があります。

計算を行うには、まずすべての牛の飼料の合計量を取得する必要があります。 これは、FodderAmountメソッドを呼び出し、牛の頭数を渡すことで行います。 さらに、この量に掛け合わせる必要がある係数も必要です。 この係数はFatteningFactorメソッドを呼び出すことで取得します。 これら2つの値と牛の頭数があれば、牛1頭あたりの餌の量(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. 牛の頭数を確認する

上の最初のタスクに取り組んでいるうちに、使っている外部ライブラリが思っていたほど高品質ではないことに気づきます。 たとえば、無効な入力を適切に処理できません。 この制限を回避するために、自分のコードで入力値のチェックを追加することにします。

DivideFoodと同じシグネチャを持つ関数ValidateInputAndDivideFoodを書きます。

  • 渡された牛の頭数が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}

カスタムエラーを手に入れたので、牛の頭数を整数で受け取り、エラー(またはnil)を返す関数ValidateNumberOfCowsを実装します。

  • 牛の頭数が0より小さい場合、関数はカスタムメッセージを"there are no negative cows"に設定したInvalidCowsErrorを返します。
  • 牛の頭数が0の場合、関数はカスタムメッセージを"no cows don't need food"に設定したInvalidCowsErrorを返します。
  • それ以外の場合、関数は検証が成功したことを示すnilを返します。
err := ValidateNumberOfCows(-5)
err.Error()
// => "-5 cows are invalid: there are no negative cows"

この検証関数を設定するという大変な作業を終えると、すでに夕方になっていることに気づき、山に沈む夕日を楽しむために机を離れます。 新しい検証関数を実際にコードに追加する作業は、また別の日に取っておくことにします。

GitHubで編集する リンクは新しいウィンドウまたはタブで開きます
Go Exercism

農場を始める準備はできましたか?

Exercismに登録すれば、34個のコンセプト166個の演習、そして本物の人間によるメンタリングとともに、Goを学んでマスターできます。すべて無料です。