Goでのオススメエラーハンドリング手法
この記事は「Eureka Advent Calendar 2020」の19日目の記事です。
こんにちは、2020年今年こそはとダイエットに意気込み、振り返ってみればジムへ行ったのは1回きり、出前を頼んだのは100回以上、見事に5kg体重を増やした山下です。
昨日は同じBackendチームのJamesさんによる「Understanding Allocations in Go」でした。
今回はこちらの記事にあるPairsエンゲージのエラー機構を詳しく説明していきたいと思います。
(本当は面白Goクイズをたくさん出したかったのですが、playgroundをmedium上にうまく埋め込めず断念しました。泣)
概要
- errorsパッケージを作成して独自のエラー構造体を定義しましょう
- エラーは全ての箇所でラップして綺麗にスタックトレースを出力しましょう
- エラーレベルやエラーメッセージを付与して汎用性を高めてみましょう
はじめに
PairsエンゲージはPairsのGoフルスクラッチを経て、新規にGoで作成されています。(アーキテクチャー等の話はこちらの記事にあります。)
新たにアプリケーションを作成するに当たって、既存のアプリケーションのエラーの取り扱いに関する、下記の課題を解決したいという想いがありました。
- エラーの原因の特定が困難
- 一度のエラーに関して、複数のエラーログが出力されてしまう
- 各所でエラーの取り扱いを考えながらコーディングする必要があり面倒
これらの課題を意識しながらエラーの取り扱い方を定めていきました。
errorsパッケージを作成して独自のエラー構造体を定義する
まずはじめに、下記のようなerrorsパッケージを作成しました。
errorsパッケージを独自で作成してそこに処理を集約することで、ビルトインされたerrorsのアップデートに追従するのもかなり楽になります。
まずはerrorの中身を格納する「message」というフィールドを用意します。さらにエラーを階層化するため、「next」というerror型のフィールドとスタックの位置情報を保存するflameというフィールドを用意しています。
Format(f State, c rune)とFormatError(p xerrors.Printer)というメソッドを用意することでfmt.Sprintfで出力した際の表示を変更しています。このあたりはお好きなように変更していただくのが良いと思います。
エラーは全ての箇所でラップして綺麗にスタックトレースを出力する
同じファイル内に下記のようなエラーを生成する関数、および、エラーをラップする関数を用意しましょう。
NewやErrorfはerrors.New
やfmt.Errorf
と同様に利用すればよいのですが、Wrapに関しては全ての箇所でWrapすることを心がけてください。おそらくこのWrapがプロジェクト内で一番呼ばれる関数になるはずです。
返り値にerrorがあればWrapです。返り値のerrorがnilでなければWrap。
if err := Exec(); err != nil {
return errors.Wrap(err)
}
このルールを守るためには下記のようなreturn時に処理を実行するような書き方はよくないですね
return Exec() // エラーをWrapできていないのでよくない
さて
ここまでできれば課題としてあげていた問題は全て解決できます。
APIでもバッチのタスクでも一番おお外の処理でエラーの出力、またはロギングの処理をしてあげればOKです。
if err := Main(); err != nil {
v := errors.AsAppError(err)
if v == nil {
v = errors.AsAppError(errors.SystemUnknown.Wrap(err, "Unknown error"))
}
fmt.Printf("%+v", v) // or ログ送信等
}
それ以外の箇所では単にラップするだけ。
【出力時一例】
github.com/eure/myapp/server/cmd/batch/cmd.runExec
/Users/yamashitakento/repos/gohome/src/github.com/eure/myapp/server/cmd/batch/cmd/batch_sample.go:38
- github.com/eure/myapp/server/src/app/batch/facades.(*SampleFacade).SampleExec
/Users/yamashitakento/repos/gohome/src/github.com/eure/myapp/server/src/app/batch/facades/sample_facade.go:99
- could not fetch : pager state &{Limit:100 Offset:0 Total:0 Max: Min: Orders:[{Column:id IsAsc:true}] pagingType:1}: :
github.com/eure/myapp/server/src/app/batch/facades.(*SampleFacade).enqueueRecommendationUsers
/Users/yamashitakento/repos/gohome/src/github.com/eure/myapp/server/src/app/batch/facades/sample_facade.go:118
- [crit] [system_default] example error!:
github.com/eure/myapp/server/src/domain/user.(*service).FindUsers
アプリケーションに合わせてより汎用性をもたせる
上述のエラー構造体には level, code, infoMessage といったフィールドも定義されています。それぞれ説明していきます。
- level
エラーにlevelという概念を持たせることで、不必要なロギングやよりクリティカルな箇所でのエラー検知等に役立ちます。
- code
エラーごとにユニークな値を持たせることでよりトラッキングしやすくなったり、code指定をルール化することでエラーのグルーピング等も用意になります。codeはstring型でもint型でもどちらでも良いと思いますが、stringならプレフィックスでグルーピングしてエラー発生時のcodeをサンプリングすることでグループごとのエラー頻度等確認することができると思います。
- infoMessage
これはエラーの構造体に「ユーザーへの表示テキスト」を持たせちゃうというやつです。横着なような気がしますが、実際管理は非常に簡単です。
ちなみにerrorsパッケージは完全にアーキテクチャの階層から独立して置いています。これはどの層にも依存せず汎用的に利用できるようにそうしています。
├── src
│ ├── api
│ ├── app
│ ├── domain
│ └── infra
└── errors
まとめ
今回紹介したエラーパッケージはGo1.12以下のバージョンの時に作成したものです。xerrorsがGoの公式から出ていて今後のアップデートを見越して作成したものであり、Go1.13のリリースの際にはxerrorsの依存を外せる予想でした。しかしスタックフレームの実装は断念され、現在もxerrorsを利用しています。今後もGo2へのプロポーザル等を確認しながら実装を追っていきますが、現時点では特に不便なく運用できています。
また、「うちはこうしている」とか、「もっとこうした方がいいよ」とか、色々意見交換ができたら非常に嬉しいです。
明日からもGoのエンジニアのブログが続きます。
是非ご拝読ください。