pkg/erorsを使ったGo言語でのエラーハンドリング

tl;dr

  • エラーを返すときにはpkg/errorserrors.Wrapでラップして返すとエラーの原因を返せる
  • エラーを受け取るときにはpkg/errorserrors.Causeで原因を見れる

前置き

以前の記事の 「エラーをチェックするだけでなく、それらを正常に処理しなさい」の項目の話を知ってから、 業務で書いているコードでも以下のようにバシバシfmt.Errorfを使っていた。

package main


import (
    "fmt"
)

func foo() error {
    err := bar()
    return fmt.Errorf("foo error: %v", err)
}

func bar() error {
    err := buz()
    return fmt.Errorf("bar error: %v", err)
}

func buz() error {
    return fmt.Errorf("buz error")
}

func main() {
    err := foo()
    if err != nil {
        fmt.Println(err)
        return
    }
}

「俺ちゃんとエラー処理してる」感を感じたのも束の間、以下のコードような課題に直面した。

package main


import (
    "fmt"
)

func foo() error {
    if err := bar(); err != nil {
        return fmt.Errorf("foo error by bar: %v", err)
    }
    if err := fatal(); err != nil {
        return fmt.Errorf("foo error by fatal: %v", err)
    }
    return nil
}

func bar() error {
    return fmt.Errorf("bar error")
}

func fatal() error {
    return fmt.Errorf("FATAL")
}

func main() {
    err := foo()
    if err != nil {
        fmt.Println(err)
        return
    }
}

一見特に問題のあるコードには見えない。しかし、エラーを受ける側が発生したエラーを判断して処理を 変更したい時に非常に困ることになる(実際困った)。

上記のコードの例だと、mainでfoo関数を実行して、そのエラーの内容を出力している。 もしエラーが発生したときは以下のように出力され、どこでどのエラーが発生したのかを知ることはできる。

foo error by bar: bar error

しかし、「foo関数で発生するエラーはそこまで致命的ではないので、翌日の朝にログとして検出できれば良いが、 fatal関数で発生するエラーは致命的で、即アラートを上げたい」というような状況が起こると少し面倒なことになる。

何が問題か

Goのエラーハンドリングの方法として、以下のように自分でエラーを定義してswitchで処理を切り替えるという方法がある。

package main

import (
    "errors"
    "fmt"
)

var (
    errBar   = errors.New("bar error")
    errFatal = errors.New("fatal error")
)

func foo() error {
    if err := bar(); err != nil {
        return errBar
    }
    if err := fatal(); err != nil {
        return errFatal
    }
    return nil
}

func bar() error {
    return errBar
}

func fatal() error {
    return errFatal()
}

func main() {
    err := foo()
    switch err {
    case errBar:
        fmt.Printf("log: %v\n", err)
    case errFatal:
        fmt.Printf("Aleart! : %v\n", err)
    default:
        fmt.Println(err)
    }
}

上記のコードの例だと、foo関数はエラー発生時に自作のエラーを返している。 外で定義されているエラーを返しているため、mainはそれをswitchで比較して動作を切り替えることができている。 しかし、この方法だとbar関数やfatal関数で発生したエラーの内容を切り捨てることになる。 もしfoo関数内で複数のエラーが発生する可能性があるとそれらも無視することになってしまう。

「そこでfmt.Errorfを使えば」と思うが、実はfmt.Errorfを使うとswitchで比較ができなくなる。

fmt.Errorfの内部を見てみると、標準パッケージのerrors.Newのラッパーになっている。 そのerrors.Newの中身はerrorString構造体にメッセージの内容を格納して 返している(ソース)。 つまり、fmt.Errorfを経由すると、全てのエラーは新たに定義されたerrorStringに変わってしまうことになり、 自分で定義したエラーとは比較ができなってしまう。

どうやって解決したか

エラーの文字列で判別するのかな?、とも一瞬思ったが、調べたらスマートなやり方があった (参考1)。 標準のerrorsパッケージに則ったライブラリのgithub.com/pkg/errorsを 使うと、発生したエラーをハンドリング元まで返すことができるようになる。

package main

import (
    "fmt"
    "github.com/pkg/errors"
)

var (
    errBar   = errors.New("bar error")
    errFatal = errors.New("fatal error")
)

func foo() error {
    if err := bar(); err != nil {
        return errors.Wrap(err, "handling bar errror")
    }
    if err := fatal(); err != nil {
        return errors.Wrap(err, "handling fatal error")
    }
    return nil
}

func bar() error {
    return errBar
}

func fatal() error {
    return errFatal
}

func main() {
    err := foo()
    switch errors.Cause(err) {
    case errBar:
        fmt.Printf("log: %v\n", err)
    case errFatal:
        fmt.Printf("Aleart! : %v\n", err)
    default:
        fmt.Println(err)
    }
}

foo関数内でエラーが発生した際、errors.Wrapに発生したエラーとメッセージを渡して返している。 errors.Wrapを使用して作成したエラーは、errors.Causeを使用して、一番最初にerrors.Wrapされたときの エラーを取り出すことが出来る。 そのため、foo関数で発生したエラーは、bar関数とfatal関数のどちらが原因なのかをmain関数で取り出すことが出来るようになり、 エラーの内容によって処理を切り替えることができるようになる。

pkg/errorsの内部実装を見てみると、errors.Wrapを使用してエラーを作成する時に、渡されたエラーを保持する構造体を 返すようになっていた。そのため、errors.Wrapを使用している限りは最初に発生したエラーは消えること無く、 errors.Causeで取り出せるようになっている。

pkg/errorsの内部は非常にシンプルな実装になっていたので、気になる人は内部の実装をちゃんと見ることをおすすめする。