Goにおけるエラーのコンテキストとエラーハンドリング
このエントリは Gopher道場 Advent Calendar 2018 5日目の記事です。
Goではエラーをerror
型の値として扱います。error
はビルトインのインタフェース型として次のように定義されています。
type error interface { Error() string }
https://golang.org/pkg/builtin/#error
Goではインタフェース宣言時に定義したメソッドリストを実装することでインタフェースを実装します。
つまり、任意の型にError() string
をメソッドとして実装すれば、独自のエラー型のerror
として扱うことが可能になります。
標準パッケージ net
のエラー型
例えば標準パッケージのnet
では、OpError
, DNSError
などの様々な独自のエラー型が定義されています。
これらのエラー型は、エラー情報を格納するための構造体フィールドがあり、エラーに関するコンテキストを持つことができます。
また、net
パッケージのエラー型はnet.Error
というインタフェース型を実装するかたちで定義されており、error
インタフェースの埋め込みの他に、エラーの性質を判別するためのTimeout() bool
, Temporary() bool
というメソッドを提供します。
type Error interface { error Timeout() bool // Is the error a timeout? Temporary() bool // Is the error temporary? }
https://golang.org/pkg/net/#Error
エラーにコンテキストとエラーの性質を判別するメソッドをもたせることによって、エラーよって処理を変えるエラーハンドリングが可能になります。
また、ネットワークエラーをあらわすエラーをnet.Error
インタフェースで集約しているため、次のように比較的スッキリしたコードで記述できます。
if err != nil { if nerr, ok := err.(net.Error); ok && nerr.Temporary() { // リトライする処理 } else { log.Fatal(err) } }
既存のエラーにコンテキストを加える
例として、次のようなWebリクエストを実行した結果を返す関数があるとします。
func SomeFunc(url string) (string, error) { resp, err := http.Get(url) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { return "", errors.New("HTTP status error: ". resp.StatusCode) } body, err := ioutil.ReadAll(resp.Body) defer resp.Body.Close() if err != nil { return "", err } return string(body[:]), nil }
この関数を別パッケージから呼び出す際には次のようなコードになると思います。
resp, err := SomeFunc("http://example.com") if err != nil { log.Fatal(err) }
上記のプログラムの追加の要件として、HTTPステータスが500 Internal Server Error
の場合にリトライ処理を実装するとします。
呼び出し元のコードでリトライ処理を行うためには、返されたエラーからHTTPステータスをチェックする必要があります。
SomeFunc()
ではresp.StatusCode != http.StatusOK
でHTTPステータスのチェックを行い200 OK
でない場合はエラーを返していますが、HTTPステータスの情報はエラーメッセージにしか含まれていません。
エラーメッセージの内容をチェックしてエラーハンドリングする処理を書くこともできますが、あまりよいやり方ではなさそうです。
また。SomeFunc()
の戻り値にHTTPステータスを追加することもできますが、APIが壊れるためこれもよくないでしょう。
このような場合に先程のnet
パッケージで定義されていたエラー型のようにコンテキストをもたせる方法が有効です。
先程のコードを書き換えてみます。
具体的には、net.Error
にならってTemporary()
メソッドをもつHTTPError
インタフェースと、それを実装するエラー型httpStatusError
を定義します。httpStatusError
はフィールドにHTTPステータスコードをもたせます。
type HTTPError interface { error Temporary() bool } type httpStatusError struct { statusCode int } func (e *httpError) Error() string { return fmt.Sprint("HTTP status error: ", e.stautsCode) } func (e *httpError) Temporary() bool { return e.stautsCode == http.StatusInternalServerError }
HTTPステータスのチェックでは、httpStatusError
型のエラーを返すようにします。
func SomeFunc(url string) (string, error) { resp, err := http.Get(url) if err != nil { return "", err } if resp.StatusCode != http.StatusOK { return "", &httpStatusError{stautsCode: resp.statusCode} } // 以下略 }
呼び出し元では次のようにエラーハンドリングすることが出来ます。
resp, err := SomeFunc("http://example.com") if err != nil { if herr, ok := err.(HTTPError); ok && herr.Temporary() { log.Errorf("retry...") // リトライ処理 } log.Fatal(err) }
まとめ
インタフェースによってエラーを判別できるようにしておくことで、呼び出し側が特定のエラー型に依存することを避けられます。