背景
エラーハンドリングでは
- エラーが発生した箇所を追うことができる
- スタックトレースが出力できればなお良し
- エラーの原因によって処理を分岐することができる
といったことが重要です。
Go 1.13から入ったラップする仕組みにより、エラーメッセージにアノテートしていくことができエラーの発生箇所を追いやすくなりました。
またerrors.Isによりエラー原因による条件分岐もしやすくなりました。
今回はそれらと独自エラーの扱いについて説明します。
環境
- Go 1.16.7
fmt.Errorfとxerrors.Errorfの違い
まず公式のfmt.Errorfとxerrors.Errorfの違いについて説明します。
共通
fmt.Errorfもxerrors.Errorfもどちらも: %s, : %v, : %wといったフォーマット文字列を扱うことができます。
: %wの場合はエラーをラップすることができ、返されたエラーはerrors.Unwrapメソッドを実装しているのでerrors.Isなどでエラーのハンドリングが可能です。
func main() { err := func1() if err != nil { fmt.Printf("%s\n", err) } if errors.Is(err, dbError) { fmt.Println("db error") } } func func1() error { err := func2() if err != nil { return fmt.Errorf("func1 error: %w", err) } return nil } func func2() error { err := root() if err != nil { return fmt.Errorf("func2 error: %w", err) } return nil } var dbError = errors.New("some error") func root() error { return dbError }
https://play.golang.org/p/YQnHElSA1SP
結果
func1 error: func2 error: some error db error
違い
xerrors.Errorfは呼び出し元のファイルと行番号を含みます。
つまりfmt.Printf("%+v", err)した時にスタックトレースを出力できます。
func main() { err := func1() if err != nil { fmt.Printf("%+v\n", err) } if errors.Is(err, dbError) { fmt.Println("db error") } } func func1() error { err := func2() if err != nil { return xerrors.Errorf("func1 error: %w", err) } return nil } func func2() error { err := root() if err != nil { return xerrors.Errorf("func2 error: %w", err) } return nil } var dbError = errors.New("some error") func root() error { return xerrors.Errorf("root error: %w", dbError) }
https://play.golang.org/p/WpfBy8a05ZJ
結果
func1 error:
main.func1
/tmp/sandbox3524972664/prog.go:23
- func2 error:
main.func2
/tmp/sandbox3524972664/prog.go:32
- root error:
main.root
/tmp/sandbox3524972664/prog.go:41
- some error
db error
注意点
ただしこのスタックトレースのフレーム(ファイルと行番号)が付くのはxerrors.Errorfでラップした場合のみです。
なのでそのままエラーを返した場合はxerrors.Errorfでラップした箇所のみスタックトレースが表示されます。
また間にfmt.Errorfを使った場合はそれより前にxerrors.Errorfを使っていてもフレーム情報が消えてしまいます。
そのままエラー返した場合
func func1() error { err := func2() if err != nil { return err } return nil } func func2() error { err := root() if err != nil { return err } return nil } var dbError = errors.New("some error") func root() error { return xerrors.Errorf("root error: %w", dbError) }
https://play.golang.org/p/uaaUEkkvC83
結果
root error:
main.root
/tmp/sandbox1423853593/prog.go:38
- some error
間にfmt.Errorfを使った場合
func func1() error { err := func2() if err != nil { return fmt.Errorf("func1 error: %w", err) } return nil } func func2() error { err := root() if err != nil { return err } return nil } var dbError = errors.New("some error") func root() error { return xerrors.Errorf("root error: %w", dbError) }
https://play.golang.org/p/jbk-x7I-iRT
結果
func1 error: root error: some error
独自エラー
公式のエラーについて整理できたので次は独自エラーの扱い方についてです。
Unwrap実装は必要?
以下のように他のエラーをラップして、errors.Isで条件分岐に使いたい場合は必要です。
func main() { err := root() if err != nil { fmt.Printf("%+v\n", err) } if errors.Is(err, dbError) { fmt.Println("db error") } } type MyError struct { msg string err error } func (m MyError) Error() string { return m.msg } func (m MyError) Unwrap() error { return m.err } func NewError(msg string) error { return MyError{ msg: msg, err: nil, } } func Wrap(err error) error { return MyError{ msg: err.Error(), err: err, } } var dbError = errors.New("some error") func root() error { return Wrap(dbError) }
https://play.golang.org/p/TBefBdFhbkN
結果
some error db error
Unwrap()を実装しないと
Unwrap()を実装していないとerrors.Isで判別できません。
func main() { err := root() if err != nil { fmt.Printf("%+v\n", err) } if errors.Is(err, dbError) { fmt.Println("db error") } } type MyError struct { msg string err error } func (m MyError) Error() string { return m.msg } func NewError(msg string) error { return MyError{ msg: msg, err: nil, } } func Wrap(err error) error { return MyError{ msg: err.Error(), err: err, } } var dbError = errors.New("some error") func root() error { return Wrap(dbError) }
https://play.golang.org/p/IvlNqCCzJ_c
結果
some error
スタックトレース情報をつけるには?
fmt.Formatterとxerrors.Formatterの2つのinterfaceを実装することで実現できます。
xerrorsのスタックトレースの仕組みに乗っかる形ですね。
fmt.Formatterについては
で分かりやすく説明されてます。
func main() { err := func1() fmt.Printf("%v\n", err) fmt.Println() fmt.Printf("%+v\n", err) } type MyError struct { message string frame xerrors.Frame } func (m *MyError) Error() string { return m.message } // Format implements fmt.Formatter func (m *MyError) Format(f fmt.State, c rune) { xerrors.FormatError(m, f, c) } // FormatError implements xerrors.Formatter func (m *MyError) FormatError(p xerrors.Printer) error { p.Print(m.message) m.frame.Format(p) return nil } func New(msg string) error { return &MyError{ message: msg, frame: xerrors.Caller(1), } } func func1() error { err := func2() if err != nil { return xerrors.Errorf("func1: %w", err) } return nil } func func2() error { err := root() if err != nil { return err } return nil } func root() error { return New("oops") }
https://play.golang.org/p/6NwoAmT0iBe
ポイントは以下です。
- structにxerrors.Frameを持つ
- fmt.Formatterインタフェースに対して、フォーマット識別子が来た時にスタックトレースを表示するのかを実装
- xerrors.Formatterインタフェースに対して、フレーム情報を出力するように実装
- 共通化してる箇所がフレームに含まれないよう
xerrors.Caller(1)に- xerrors.Callerはスタックフレームをスキップできる
結果
エラーの発生箇所と、xerrors.Errorfでラップした箇所のフレーム情報が出力されます。
xerrorsの仕組みに乗っかってるので、先程のxerrors.Errorfでの注意点と同じです。
func1: oops
func1:
main.func1
/tmp/sandbox3441195528/prog.go:47
- oops:
main.root
/tmp/sandbox3441195528/prog.go:61
どれを使えばいいか
選択肢としては以下があります。
スタックトレースが要らない場合
fmt.Errorfで十分かと思います。
ただしエラーの発生箇所を追うために各所のエラーハンドリングでラップする必要があります。
スタックトレースが要る場合
a) シンプルな用途
xerrors.Errorfで良いと思います。
ただしフレーム情報を増やすには各所のエラーハンドリングでラップする必要があります。
b) 独自のエラーコードを使いたい
のような独自のエラーコードでハンドリングしたい場合は独自エラーを定義し、
- xerrorsのスタックトレースの仕組みに乗っかる
という今回のようなやり方か、
- pkg/errors.WithStackでフレーム情報を持つ
が良いと思います。
前者はフレーム情報を積むためにラップする処理が必要ですが、後者は不要です。
まとめ
fmt.Errorfとxerrors.Errorfの違いと、独自エラーを使う場合に必要な実装について説明しました。