Goでカスタムエラーを作成する
序章
Goは、標準ライブラリにエラーを作成するための2つのメソッド、errors.Newとfmt.Errorfを提供します。 より複雑なエラー情報をユーザーに、またはデバッグ時に将来の自分に伝える場合、これら2つのメカニズムでは、何が起こったかを適切にキャプチャして報告するには不十分な場合があります。 このより複雑なエラー情報を伝達し、より多くの機能を実現するために、標準ライブラリインターフェイスタイプエラーを実装できます。
この構文は次のようになります。
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
を使用してこれを印刷します。
RequestError
のError()
メソッド内で、fmt.Sprintf
関数を使用して、エラーの作成時に提供された情報を使用して文字列を作成します。
タイプアサーションとカスタムエラー
error
インターフェイスは1つのメソッドのみを公開しますが、エラーを適切に処理するには、error
実装の他のメソッドにアクセスする必要がある場合があります。 たとえば、error
のカスタム実装がいくつかあり、それらは一時的で再試行できます。これは、Temporary()
メソッドの存在によって示されます。
インターフェイスは、タイプによって提供されるメソッドの幅広いセットへの狭いビューを提供するため、タイプアサーションを使用して、ビューが表示するメソッドを変更するか、ビューを完全に削除する必要があります。
次の例では、前に示した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()
内で、doRequest()
を呼び出し、error
インターフェイスを返します。 まず、Error()
メソッドによって返されたエラーメッセージを出力します。 次に、タイプアサーションre, ok := err.(*RequestError)
を使用して、RequestError
のすべてのメソッドを公開しようとします。 タイプアサーションが成功した場合は、Temporary()
メソッドを使用して、このエラーが一時的なエラーであるかどうかを確認します。 doRequest()
によって設定されたStatusCode
は503
であり、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
は、string
としてのコンテキストメッセージと、このWrappedError
が詳細情報を提供するerror
の2つのフィールドを持つ構造体です。 Error()
メソッドが呼び出されると、再びfmt.Sprintf
を使用してコンテキストメッセージを出力し、error
(fmt.Sprintf
は暗黙的にError()
メソッドも)。
main()
内で、errors.New
を使用してエラーを作成し、定義したWrap
関数を使用してそのエラーをラップします。 これにより、このerror
が"main"
で生成されたことを示すことができます。 また、WrappedError
もerror
であるため、他のWrappedError
をラップできます。これにより、エラーの原因を突き止めるのに役立つチェーンを確認できます。 。 標準ライブラリの助けを少し借りれば、完全なスタックトレースをエラーに埋め込むこともできます。
結論
error
インターフェースは単一の方法にすぎないため、さまざまな状況でさまざまなタイプのエラーを提供する際に大きな柔軟性があることがわかりました。 これには、エラーの一部として複数の情報を伝達することから、指数バックオフを実装することまで、すべてを網羅できます。 Goのエラー処理メカニズムは一見単純に見えるかもしれませんが、これらのカスタムエラーを使用して、一般的な状況と一般的でない状況の両方を処理することで、非常に豊富な処理を実現できます。
Goには、予期しない動作であるパニックを伝える別のメカニズムがあります。 エラー処理シリーズの次の記事では、パニックとは何か、パニックの処理方法について説明します。