Goでカスタムエラーを作成する

前書き

Goには、標準ライブラリにエラーを作成するための2つのメソッドerrors.New and fmt.Errorfが用意されています。 より複雑なエラー情報をユーザーに、またはデバッグ時に将来の自分に伝える場合、これらの2つのメカニズムでは、何が起こったのかを適切にキャプチャして報告するには不十分な場合があります。 このより複雑なエラー情報を伝達し、より多くの機能を実現するために、標準ライブラリインターフェイスタイプerrorを実装できます。

この構文は次のとおりです。

type error interface {
  Error() string
}

builtinパッケージは、エラーメッセージを文字列として返す単一のError()メソッドを持つインターフェイスとしてerrorを定義します。 このメソッドを実装することにより、定義した型を独自のエラーに変換できます。

次の例を実行して、errorインターフェイスの実装を確認してみましょう。

package main

import (
    "fmt"
    "os"
)

type MyError struct{}

func (m *MyError) Error() string {
    return "boom"
}

func sayHello() (string, error) {
    return "", &MyError{}
}

func main() {
    s, err := sayHello()
    if err != nil {
        fmt.Println("unexpected error: err:", err)
        os.Exit(1)
    }
    fmt.Println("The string:", s)
}

次の出力が表示されます。

Outputunexpected error: err: boom
exit status 1

ここでは、新しい空の構造体タイプMyErrorを作成し、その上にError()メソッドを定義しました。 Error()メソッドは、文字列"boom"を返します。

main()内で、空の文字列とMyErrorの新しいインスタンスを返す関数sayHelloを呼び出します。 sayHelloは常にエラーを返すため、main()のifステートメントの本体内のfmt.Println呼び出しは常に実行されます。 次に、fmt.Printlnを使用して、err変数内に保持されているMyErrorのインスタンスとともに短いプレフィックス文字列"unexpected error:"を出力します。

fmtパッケージは、これがerrorの実装であることを自動的に検出できるため、Error()を直接呼び出す必要がないことに注意してください。 Error()transparentlyを呼び出して文字列"boom"を取得し、プレフィックス文字列"unexpected error: err:"と連結します。

カスタムエラーで詳細情報を収集する

カスタムエラーは、詳細なエラー情報を取得する最もクリーンな方法である場合があります。 たとえば、HTTPリクエストによって生成されたエラーのステータスコードをキャプチャするとします。次のプログラムを実行して、その情報をクリーンにキャプチャできるerrorの実装を確認します。

package main

import (
    "errors"
    "fmt"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return fmt.Sprintf("status %d: err %v", r.StatusCode, r.Err)
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        os.Exit(1)
    }
    fmt.Println("success!")
}

次の出力が表示されます。

Outputstatus 503: err unavailable
exit status 1

この例では、RequestErrorの新しいインスタンスを作成し、標準ライブラリのerrors.New関数を使用してステータスコードとエラーを提供します。 次に、前の例のようにfmt.Printlnを使用してこれを出力します。

RequestErrorError()メソッド内で、fmt.Sprintf関数を使用して、エラーの作成時に提供された情報を使用して文字列を作成します。

型アサーションとカスタムエラー

errorインターフェースは1つのメソッドのみを公開しますが、エラーを適切に処理するために、error実装の他のメソッドにアクセスする必要がある場合があります。 たとえば、Temporary()メソッドの存在によって示される、一時的で再試行可能なerrorのカスタム実装がいくつかある場合があります。

インターフェイスは、型によって提供されるメソッドの幅広いセットへの狭いビューを提供するため、ビューが表示しているメソッドを変更するか、完全に削除するには、type assertionを使用する必要があります。

次の例では、前に示したRequestErrorを拡張して、呼び出し元が要求を再試行する必要があるかどうかを示すTemporary()メソッドを使用します。

package main

import (
    "errors"
    "fmt"
    "net/http"
    "os"
)

type RequestError struct {
    StatusCode int

    Err error
}

func (r *RequestError) Error() string {
    return r.Err.Error()
}

func (r *RequestError) Temporary() bool {
    return r.StatusCode == http.StatusServiceUnavailable // 503
}

func doRequest() error {
    return &RequestError{
        StatusCode: 503,
        Err:        errors.New("unavailable"),
    }
}

func main() {
    err := doRequest()
    if err != nil {
        fmt.Println(err)
        re, ok := err.(*RequestError)
        if ok {
            if re.Temporary() {
                fmt.Println("This request can be tried again")
            } else {
                fmt.Println("This request cannot be tried again")
            }
        }
        os.Exit(1)
    }

    fmt.Println("success!")
}

次の出力が表示されます。

Outputunavailable
This request can be tried again
exit status 1

main()内で、errorインターフェイスを返すdoRequest()を呼び出します。 最初に、Error()メソッドによって返されたエラーメッセージを出力します。 次に、タイプアサーションre, ok := err.(*RequestError)を使用して、RequestErrorのすべてのメソッドを公開しようとします。 タイプアサーションが成功した場合は、Temporary()メソッドを使用して、このエラーが一時的なエラーであるかどうかを確認します。 doRequest()によって設定されたStatusCode503であり、これはhttp.StatusServiceUnavailableと一致するため、これはtrueを返し、"This request can be tried again"を出力します。 実際には、メッセージを出力するのではなく、代わりに別の要求を作成します。

ラッピングエラー

一般的に、データベース、ネットワーク接続など、プログラムの外部からエラーが生成されます。 これらのエラーから提供されるエラーメッセージは、エラーの原因を特定するのに役立ちません。 エラーメッセージの先頭にエラーを追加情報でラップすると、デバッグを成功させるために必要なコンテキストが提供されます。

次の例は、他の関数から返された不可解なerrorにコンテキスト情報を添付する方法を示しています。

package main

import (
    "errors"
    "fmt"
)

type WrappedError struct {
    Context string
    Err     error
}

func (w *WrappedError) Error() string {
    return fmt.Sprintf("%s: %v", w.Context, w.Err)
}

func Wrap(err error, info string) *WrappedError {
    return &WrappedError{
        Context: info,
        Err:     err,
    }
}

func main() {
    err := errors.New("boom!")
    err = Wrap(err, "main")

    fmt.Println(err)
}

次の出力が表示されます。

Outputmain: boom!

WrappedErrorは、2つのフィールドを持つ構造体です。stringとしてのコンテキストメッセージと、このWrappedErrorが詳細情報を提供しているerrorです。 Error()メソッドが呼び出されると、再びfmt.Sprintfを使用してコンテキストメッセージを出力し、errorfmt.SprintfError()メソッドを暗黙的に呼び出すことを認識します。上手)。

main()内で、errors.Newを使用してエラーを作成し、次に定義したWrap関数を使用してそのエラーをラップします。 これにより、このerror"main"で生成されたことを示すことができます。 また、WrappedErrorerrorであるため、他の `WrappedError`をラップできます。これにより、エラーの原因を追跡するのに役立つチェーンを確認できます。 標準ライブラリの少しの助けを借りて、完全なスタックトレースをエラーに埋め込むこともできます。

結論

errorインターフェースは単一のメソッドにすぎないため、さまざまな状況でさまざまなタイプのエラーを提供する際に大きな柔軟性があることがわかりました。 これには、エラーの一部として複数の情報を伝達することから、exponential backoffを実装することまで、すべてが含まれます。 Goのエラー処理メカニズムは一見単純に見えるかもしれませんが、これらのカスタムエラーを使用して非常に豊富な処理を実現し、一般的な状況とまれな状況の両方を処理できます。

Goには、パニックという予期しない動作を伝える別のメカニズムがあります。 エラー処理シリーズの次の記事では、パニックを検証します。パニックとは何か、その処理方法を説明します。