LoginSignup
33
22

More than 5 years have passed since last update.

Go で静的解析して Go 1.13 から標準の xerrors とうまく付き合っていく

Posted at

はじめに

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)で構成されています。

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 - GoDoc
ssa - GoDoc

特に静的解析, analysis 周りの勉強は以下を活用させていただきました。
ありがとうございます。

Goにおける静的解析のモジュール化について - Mercari Engineering Blog
GoのためのGo

33
22
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
33
22