Go でのエラー処理の方法

完了

プログラムの作成中は、プログラムが失敗する可能性のあるさまざまな状況について考える必要があります。また、失敗を管理する必要があります。 ユーザーが、長く複雑なスタック トレース エラーを確認する必要はありません。 どのような問題が発生したかについての有意義な情報を確認する方が有益です。 既に説明したように、Go には、プログラム内で例外または予想しない動作を管理するための、panicrecover などの組み込み関数が用意されています。 ただし、エラーとは、既知の失敗であり、プログラムはそれを処理するように構築されている必要があります。

Go でのエラー処理の手法は、if および return ステートメントのみが必要とされる、単なる制御フロー機構です。 たとえば、employee オブジェクトから情報を取得するための関数を呼び出しているときに、その従業員が存在するかどうかの確認が必要となる場合があります。 予想されるエラーを処理するための、Go ならではの方法とは、次のようなものです。

employee, err := getInformation(1000)
if err != nil {
    // Something is wrong. Do something.
}

getInformation 関数がどのように employee 構造体を返すか、またどのようにエラーを 2 番目の値として返すかについて注目してください。 エラーが nil の場合があります。 エラーが nil の場合は、成功を意味します。 nil ではない場合は、失敗を意味します。 nil 以外のエラーには、出力できる、またはログに記録できるエラー メッセージが表示されます。 これが、Go でエラーを処理する方法です。 その他のいくつかの方法については、次のセクションで説明します。

Go でのエラー処理では、エラーの報告と処理の方法に注意が必要であることに気が付くかもしれません。 まさにそこがポイントです。 その他の例を確認して、Go でのエラー処理の手法をより深く理解しましょう。

構造体に使用したコード スニペットを使用して、さまざまなエラー処理方法を実践していきます。

package main

import (
    "fmt"
    "os"
)

type Employee struct {
    ID        int
    FirstName string
    LastName  string
    Address   string
}

func main() {
    employee, err := getInformation(1001)
    if err != nil {
        // Something is wrong. Do something.
    } else {
        fmt.Print(employee)
    }
}

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    return employee, err
}

func apiCallEmployee(id int) (*Employee, error) {
    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

ここからは、エラー処理方法を説明するために、getInformationapiCallEmployeemain の各関数の変更を重点的に説明します。

エラー処理方法

関数がエラーを返す場合、通常はそれが最後の戻り値になります。 前のセクションで見たように、エラー発生の有無を確認し、それを処理するのは、呼び出し元の責任です。 そのため、継続的にそのパターンを使用し、サブルーチンでエラーを伝達するのが一般的な方法です。 たとえば、他の操作を実行しなくても、次のように、サブルーチン (前の例の getInformation など) が呼び出し元にエラーを返す場合があります。

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, err // Simply return the error to the caller.
    }
    return employee, nil
}

さらに多くの情報を含めたうえで、エラーを伝達することもできます。 その目的のために、fmt.Errorf() 関数を使用することもできます。これは、前に説明したものと似ていますが、これはエラーを返します。 たとえば、次のように、さらに多くのコンテキストをエラーに追加しても、元のエラーを返すことができます。

func getInformation(id int) (*Employee, error) {
    employee, err := apiCallEmployee(1000)
    if err != nil {
        return nil, fmt.Errorf("Got an error when getting the employee information: %v", err)
    }
    return employee, nil
}

もう 1 つの方法は、一時的なエラーの場合に、再試行ロジックを実行することです。 たとえば、次のように、再試行ポリシーを使用して関数を 3 回呼び出し、2 秒間待ちます。

func getInformation(id int) (*Employee, error) {
    for tries := 0; tries < 3; tries++ {
        employee, err := apiCallEmployee(1000)
        if err == nil {
            return employee, nil
        }

        fmt.Println("Server is not responding, retrying ...")
        time.Sleep(time.Second * 2)
    }

    return nil, fmt.Errorf("server has failed to respond to get the employee information")
}

最後に、コンソールにエラーを出力する代わりに、エラーをログに記録して、エンド ユーザーに対して実装の詳細をすべて非表示にすることができます。 ログ記録については、次のモジュールで説明します。 ここでは、カスタム エラーを作成して使用する方法について見てみましょう。

再利用可能なエラーの作成

エラー メッセージの数が増えてしまい、秩序を保つ必要がある場合があります。 または、再利用する一般的なエラー メッセージのライブラリの作成が必要となる場合もあります。 Go では、次のように、errors.New() 関数を使用してエラーを作成し、それらをいくつかの部分で再利用することができます。

var ErrNotFound = errors.New("Employee not found!")

func getInformation(id int) (*Employee, error) {
    if id != 1001 {
        return nil, ErrNotFound
    }

    employee := Employee{LastName: "Doe", FirstName: "John"}
    return &employee, nil
}

getInformation 関数のコードがわかりやすくなりました。エラー メッセージを変更する必要がある場合は、1 か所のみで行います。 また、規則では、エラー変数に Err プレフィックスを含めることになっているため、注意してください。

最後に、エラー変数がある場合は、呼び出し元の関数でエラー処理しているときに、より具体的な値を指定することができます。 errors.Is() 関数を使用すると、次のように、発生しているエラーの種類を比較することができます。

employee, err := getInformation(1000)
if errors.Is(err, ErrNotFound) {
    fmt.Printf("NOT FOUND: %v\n", err)
} else {
    fmt.Print(employee)
}

Go でエラーを処理する場合は、次のような推奨事項を考慮してください。

  • エラーが予想されていなくても、エラーがないかどうかを常に確認します。 そのうえで、それらを適切に処理して、不要な情報がエンド ユーザーに公開されないようにします。
  • エラー メッセージにプレフィックスを含めて、エラーの発生元がわかるようにします。 たとえば、パッケージや関数の名前を含めることができます。
  • 可能な限り、再利用可能なエラー変数を作成します。
  • エラーを返すことと、パニックの違いを理解します。 他に対処方法がない場合は、パニックが発生します。 たとえば、依存関係の準備ができていない場合、プログラムは動作しません (既定の動作を実行する場合を除きます)。
  • 可能な限り多くの詳細情報でエラーをログに記録し (次のセクションでその方法を説明します)、エンド ユーザーが理解可能なエラーを出力します。