Yuichi Murata's Engineering Blog

Go / App Engine / GCP とか書いていきます

Go の 3 つのエラーハンドリングパターン

今回は go のエラーハンドリングのパターンについて書いていきたいと思います。以下の Post とかなり被る部分があるので合わせて参考にしてください。

Error handling and Go - The Go Blog

Pattern 1: エラーを値として定義する

一番単純な方法としてエラーを値として定義してしまう方法があります。

var ErrNeedCake = errors.New("error - i need cake not this")
func Func1(name string) error {
        if name != "cake" {
                return ErrNeedCake
        }
        return nil
}

Func1() は引数 name をとり、引数がケーキでなければエラーを返すというシンプルな関数です。ここで ErrNeedCake というエラー構造体の値をパッケージ変数として定義しておきます。この方法を使うと、最もシンプルにエラーを定義することが出来ます。以下のようにすれば、呼び出し元のエラー判定にも使うことが出来ます。

func main() {
        if err := Func1("chocolate"); err != nil {
                if err == ErrNeedCake {
                        fmt.Printf("Hmm, he seems to need a cake\n")
                } else {
                        fmt.Printf("Got unexpected error!\n")
                        panic(err)
                }
        }
}

いっぽう、固定のエラーメッセージしか返すことが出来ないため、エラーの内容の詳細を伝えるとができません。

Pattern 2: 動的にエラー値を生成する

fmt パッケージには Printf と同様のフォーマットでお気軽に error 値を作ることができます。これを用いて動的にエラー値を作ることができます。

func Func2(name string) error {
    if name != "cake" {
        return fmt.Errorf("error - i need cake not %s", name)
    }
    return nil
}

おそらく最も簡単なエラーの返し方でしょう。また、メッセージを動的に生成できるのでより詳細な情報を返すことができます。

func main() {
    fmt.Println("\n== Pattern 2: Creates ad-hok ==")
    if err := Func2("banana"); err != nil {
        fmt.Printf("Func2() got error: %v\n", err)
    }
}

ただしこの方法はエラーの内容を表示する分には良いですが、分岐させることが難しいことです。簡単な使い捨てプログラムを書いているときや、クライアントが賢く分岐しないような局面では最もシンプルな戦略です。

Pattern 3: 独自エラー型を定義する

最も真面目でしっかりとエラーハンドリングをするには error interface を実装する独自のエラー型を定義することです。

type ErrNeedElse struct {
    Got  string
    Need string
}

func (e ErrNeedElse) Error() string {
    return fmt.Sprintf("error - i need %s not %s", e.Need, e.Got)
}

func Func3(name string) error {
    if name != "cake" {
        return ErrNeedElse{name, "cake"}
    }
    return nil
}

この方法を使うと動的に詳しいエラーメッセージを生成して表示できるようになるだけではなく、エラー型や詳細なフィールドを返すことができます。これを用いてより賢いエラーハンドリングをすることが可能です。

func main() {
    if err := Func3("strawberry"); err != nil {
        switch err := err.(type) {
        case ErrNeedElse:
            fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
        default:
            fmt.Printf("Got unexpected error!\n")
            panic(err)
        }
    }
}

ちょっと手間ではありますが、再利用性の高いパッケージや複雑なエラーを返すパッケージはこの方法を検討すべきです。

Pattern 3 (番外編): 独自エラー型はかならず error interface で返す

状況によっては error interface ではなく、以下の様に独自エラー型をそのあまま値として返したい衝動に駆られます。

func Func4(name string) *ErrNeedElse {
        if name != "cake" {
                return &ErrNeedElse{name, "cake"}
        }
        return nil
}

もし返されるエラー型が独自型に限定されるのであれば、型アサーションの必要がありません。呼び出し元のエラーハンドリングも簡単になります。この場合、以下のようなシンプルな構成にすることが可能です。

func main() {
    if err := Func3("strawberry"); err != nil {
                 fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
    } 
}

しかし、この方法はあまりおすすめ出来ないです。Go 言語は error interface を使うことによって、様々なパッケージのエラーハンドリングを共通化することができています。しかし、独自のエラー型の値をインタフェースとすることなく、そのまま返してしまうと辻褄の合わないケースがでてきます。例えば以下のケースを見てみましょう。

func Func4(name string) *ErrNeedElse {
        if name != "cake" {
                return &ErrNeedElse{name, "cake"}
        }
        return nil
}

type GiftToYou struct {
        Name string `json:name`
}

func (g *GiftToYou) UnmarshalJSON(data []byte) error {
        type alias *GiftToYou
        var this alias = alias(g)
        json.Unmarshal(data, &this)

        return Func4(g.Name)
}

ある構造体 GiftToYou の UnmarshalJSON() 関数を定義することによって、json の Bind 時に独自のロジックを走らせることができます。この時にエラーが発生したら、エラーを返すという処理を考えてみましょう。呼び出し側はこのようにしてみます。

func main() {
        v := GiftToYou{}
        if err := json.Unmarshal([]byte(`{"name":"cake"}`), &v); err != nil {
                switch err := err.(type) {
                case *ErrNeedElse:
                        fmt.Printf("Hmm, he seems to need %s not %s\n", err.Need, err.Got)
                default:
                        fmt.Printf("Got unexpected error!\n")
                        panic(err)
                }
        }
}

さて、このプログラムを実行すると、正常に処理が終了するはずが、なぜか以下の様な panic が置きます。

panic: runtime error: invalid memory address or nil pointer dereference
[signal 0xb code=0x1 addr=0x0 pc=0x30af]

goroutine 1 [running]:
panic(0x10d8c0, 0xc82000a1d0)
        /usr/local/Cellar/go/1.6.2/libexec/src/runtime/panic.go:481 +0x3e6
main.main()
        /Users/yuichi.murata/go/src/github.com/yuichi1004/go-error-handling/main.go:100 +0xb3f
exit status 2

結論から言うとこれは nil の方情報が一致しないためです。go の nil には型情報があって、Func4() の戻り値に nil が返された場合、それは error interface の nil ではなくて、*ErrNeedElse の nil という扱いになってしまいます。それが最終的に json.Unmarshal() の戻り値で返ってきたときに、型の不一致で正しく判定できなくなってしまうのです。

nil の不思議な挙動は例えば以下のエントリーで詳しく解説されています。

絶対ハマる、不思議なnil - Qiita

結論

  • 簡単な分岐のみが必要な場合はエラーを値として定義する
  • 再利用性のないコードであれば動的にエラー値を生成する
  • ある程度再利用性のあるコードは独自エラー型を定義して error interface として返すべし