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) }
まとめ
インタフェースによってエラーを判別できるようにしておくことで、呼び出し側が特定のエラー型に依存することを避けられます。