2019年10月7日月曜日

golang で Wrap と Cause を使ったエラーハンドリングの方法

概要

github.com/pkg/errors の Wrap と Cause を使ったエラーハンドリングのサンプルを紹介します
Wrap と Cause 自体はエラーのラップとラップ前の大元のエラーを取得するための関数です

環境

  • macOS 10.14.6
  • golang 1.12.9

普通に構造体で判定する

まずは普通にエラーの構造体を作成して switch 文で判定してみます
errors パッケージも Wrap と Cause が使えない標準の errors パッケージになります

package main

import (
    "errors"
    "fmt"
    "reflect"
)

type Error1 struct{}
type Error2 struct{}

func (e1 *Error1) Error() string {
    return fmt.Sprintf("error1")
}

func (f *Error2) Error() string {
    return fmt.Sprintf("error2")
}

func fetch(t int) error {
    if t == 1 {
        return &Error1{}
    } else if t == 2 {
        return &Error2{}
    } else {
        return errors.New("errorString")
    }
}

func main() {
    err := fetch(0) // 1 or 2 or other number
    fmt.Println(reflect.TypeOf(err))
    switch err.(type) {
    case *Error1:
        fmt.Println("error1 occured")
        fmt.Println(err)
    case *Error2:
        fmt.Println("error2 occured")
        fmt.Println(err)
    default:
        fmt.Println("errorString occured")
        fmt.Println(err)
    }
}

標準の errors パッケージのタイプを見ると *errors.errorString というタイプになっているようです

少し解説

Error() 関数を持った構造体を定義することで自作のエラー構造体として扱うことができます
fetch 関数では error タイプとして返却することができます

あとは受け取ったエラーを err.(type) でタイプ判定するだけです
一番オーソドックスな判定方法かなと思います

Wrap + Cause で大元の構造体を判定する

次に github.com/pkg/errors を使ってみます
これを使うことでエラーをラップしエラーが発生した経路を把握しやすくすることができます

package main

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

type Error1 struct{}
type Error2 struct{}

func (e1 *Error1) Error() string {
    return fmt.Sprintf("error1")
}

func (f *Error2) Error() string {
    return fmt.Sprintf("error2")
}

func fetch(t int) error {
    if t == 1 {
        e1 := &Error1{}
        e2 := &Error2{}
        return errors.Wrap(e1, e2.Error())
    } else if t == 2 {
        e2 := &Error2{}
        e1 := &Error1{}
        return errors.Wrap(e2, e1.Error())
    } else {
        return errors.New("fundamental")
    }
}

func main() {
    err := fetch(0) // 1 or 2 or other number
    fmt.Println(reflect.TypeOf(err))
    switch errors.Cause(err).(type) {
    case *Error1:
        fmt.Println("error1 occured")
        fmt.Println(err)
    case *Error2:
        fmt.Println("error2 occured")
        fmt.Println(err)
    default:
        fmt.Println("fundamental error occured")
        fmt.Println(err)
    }
}

github.com/pkg/errors パッケージを New すると errors.fundamental というタイプになりました

少し解説

errors.Wrap は既存のエラーにエラー文を追加することができる関数です
単純に末尾にエラー文が追加されます
大元のエラーのタイプは変わりません

大元のエラーのタイプを取り出す場合は errors.Cause を使います
これで大元のエラーを取り出してあとは type でタイプを参照すれば OK です

結局エラーの型を知らないとダメかな

エラーハンドリングする際は結局エラーの型を知らないと判定できないかなと思います
もしくはエラー文で分岐する方法もありますがその方法はほとんど見たことがありません
基本は error で返ってきて .(type) でキャストするとタイプがわかる感じなのでそれを使うのが簡単かなと思います
特にライブラリを使っている場合はこの方法になるかなと思います

どんなタイプのエラーが返ってくるはさすがにリファレンスやコードを読むしかないかなと思います
もしくはエラーを発生させられる環境があるのであれば サンプルにあるように fmt.Println(reflect.TypeOf(err)) でタイプを見ても良いかなと思います

もしくはインタフェースを実装するか

Cause とインタフェースを組み合わせてエラーを判定する方法もあるようです (参考)
自作のエラー構造体が特定のインタフェースを実装しているかどうかでエラーのタイプを判定します

0 件のコメント:

コメントを投稿