shiimaxx's blog

最も愛を大切に

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)
}

まとめ

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

参考文献