はじめに
Goの新しいerrors パッケージ xerrors(Go 1.13からは標準のerrorsパッケージに入る予定) - Qiita
上記の @sonatard さんの記事にある通り、 Go 1.13 から golang.org/x/xerrors が標準の error パッケージとして入るそうです。
標準でエラーを追いやすい形の仕組みができるのは嬉しいですね。
この記事では xerrors と使用する上で発生しそうな問題と、
問題を防ぐために静的解析してチェックする lint を作成しましたので紹介します。
pkg/errors との違いと感じている問題
スタックフレームを明示的に積んでいく必要がある
今までは github.com/pkg/errors を使用してエラー伝搬してスタックトレースを表示させるのが主流だったかと思います。
pkg/errors
では errors.New
, errors.Wrap
によって、それが呼び出された際のスタックトレースが error
として帰ってきていました。
しかし、 xerrors
では明示的に wrap しない限り、 stack frame を積んでいきません。
故に以下のような問題が起こりそうです。
- Wrap し忘れてフレームが抜けてしまう
- Wrap の構文を間違えてフレームが抜けてしまう
後者に関しては @tenntenn さんの作成した wrapperfmt を使用する事によって防げそうです。
error を比較する際に Is, As を使う必要がある
こちらに関しては今まで pkg/errors
を使っていた際にもあった問題だと思います。
errors.Cause
で root のエラーを取得して比較するというのが必要ですが、
実装者のミスによって、 そのまま ==
, !=
で比較してしまいエラーが適切に捕捉できないということが起こりえます。
xerrors
においても正しく error を捕捉するためには xerrors.Is
もしくは xerrors.As
を使用する必要があります。
どう解決できるか?
- Wrap し忘れてフレームが抜けてしまう問題
-
error
を間違えて==
,!=
で比較してしまう問題
これらは以下のようにコーディングルールに落とし込めそうです。
-
error
を返却する際にはxerrors.Errorf
によって wrap する -
error
を比較する際には==
,!=
を使用しない
もちろんこれらのコーディングルールはレビューの際に指摘することができれば防ぐ事ができます。
しかし、レビューするのもまた人間です。人間は間違いを起こします。
コーディングルールに落とし込めるのであれば静的解析によってチェックすることができそうです。
静的解析する lint を作成すれば、CI に組み込むこともできます。
幸いなことに Go では静的解析をする仕組みが標準で整っていますし、 analysis という静的解析をするためのフレームワークも公式が提供しています。
今回はこちらと、 @tenntenn さんの analysis のコードジェネレータである skeleton を使用して lint ツールを作成しました。
作成した lint ツール
xerrchk というものを作成しました。
$ go get github.com/sachaos/xerrchk/cmd/xerrchk
これは複数のモジュール(analysis で言うところの pass)で構成されています。
- sachaos/xerrchk/passes/wrapping
- sachaos/xerrchk/passes/isas
- tenntenn/gosa/passes/nilerr (@tenntenn さん作)
- tenntenn/gosa/passes/wraperrfmt (@tenntenn さん作)
wrapping
モジュール
wrapping
では関数が error
を返却する際に xerrors.Errorf
, もしくは xerrors.Opaque
によって wrap しているかをチェックします。
例えば以下のソースコードでは wrap2
関数において wrap できていません。
package main
import (
"fmt"
"golang.org/x/xerrors"
)
var sentinelErr = xerrors.Errorf("sentinel error")
func main() {
fmt.Printf("%+v\n", wrap1())
}
func wrap1() error {
return xerrors.Errorf("error on wrap1: %w", wrap2())
}
func wrap2() error {
return alwaysErr()
}
func alwaysErr() error {
return xerrors.Errorf("error on alwaysErr: %w", sentinelErr)
}
こちらの結果は以下のようになります。
wrap2 のフレームはなく、どこを通ったのかが stack frame を見る限りではわかりません。
(この例の場合は自明ですが・・・)
error on wrap1:
main.wrap1
/Users/takumasa.sakao/dev/github.com/sachaos/go-sandbox/xerrchk/main.go:16
- error on alwaysErr:
main.alwaysErr
/Users/takumasa.sakao/dev/github.com/sachaos/go-sandbox/xerrchk/main.go:24
- sentinel error:
main.init
/Users/takumasa.sakao/dev/github.com/sachaos/go-sandbox/xerrchk/main.go:9
このコードに適応すると以下のように、 wrap されていないところを lint が指摘してくれます。
$ xerrchk -wrapping .
/path/to/src/main.go:20:18: wrap with xerrros.Errorf or xerrors.Opaque
どの粒度で wrap するべきか?
全ての関数の返り値を wrap するというのが適切とは限りません。
深くネストされたところから返ってくる error
はフレーム数が多く、スタックフレームは見にくくなりそうです。
そこでどの粒度で wrap するべきかというのは考えたほうが良さそうです。
個人的には外部から呼ばれる可能性のある public な export された関数では wrap を必須として private な関数に関しては wrap を必須としないというのがいい塩梅なのではないかと思っています。
xerrchk
では scope
というオプションがあり、 こちらでチェック範囲を制御することができます。
# public のみを対象とする
$ xerrchk -wrapping.scope=public
# 全ての関数を対象とする(default)
$ xerrchk -wrapping.scope=all
isas
モジュール
isas
モジュールでは ==
, !=
を使用して error
を比較しているところを見つけます。
例えば以下のコードでは正しく sentinelErr
を捕捉することができず、 failed to handle error
と出力されます。
package main
import (
"fmt"
"golang.org/x/xerrors"
)
var sentinelErr = xerrors.Errorf("sentinel error")
func main() {
err := wrap()
if err == sentinelErr {
fmt.Printf("handle sentinel err")
}
fmt.Printf("failed to handle error")
}
func wrap() error {
return xerrors.Errorf("error on wrap1: %w", alwaysErr())
}
func alwaysErr() error {
return xerrors.Errorf("error on alwaysErr: %w", sentinelErr)
}
これを isas
に適応すると xerrors.Is
を使用するようにと注意をしてくれます。
$ xerrchk -isas .
/Users/takumasa.sakao/dev/github.com/sachaos/go-sandbox/xerrchk/main.go:13:5: do not compare error with "==" or "!="
おわりに
xerrors
の使用で気をつけなければならなさそうなところと、それを静的解析でチェックすることができる xerrchk
を紹介させていただきました。
xerrors
便利なので安全に使っていきたいですね。
なお、 xerrchk
は実際の大規模なソースコードに適応してテストしたわけではないので、考慮漏れ・不具合などなどあるかと思います。
もし使用して見つけた方は GitHub で issue 報告していただけるか PR を送っていただけるとありがたいです。
また xerrors
は go 1.13 になれば標準に入る予定とのことなので、 その際にはこのような lint も公式でサポートされるといいですね。
参考文献
特に静的解析, analysis 周りの勉強は以下を活用させていただきました。
ありがとうございます。