blog.syfm

徒然なるままに考えていることなどを書いていくブログ

pkg/errors から徐々に Go 1.13 errors へ移行する

Go 1.13 からの新しい errors

Go 1.13 からエラー処理が強化され、errors パッケージに As と Is 関数が追加されました。これにより、今までは pkg/errors のようなライブラリを使用しなければ実現するのが難しかった、型情報を保持したままの wrap/unwrap が可能になりました。

サードパーティのライブラリに依存せずに実現できるのは非常にありがたい反面、移行コストが高そうでなかなか移行できていないプロジェクトも多いかと思います。また、pkg/errors ではスタックトレース情報を保持してくれますが、Go 1.13 errors では保持してくれないといった問題もあるため、完全移行が難しいといったケースもあると思います。

pkg/errors の Go 1.13 errors 対応

しかし最近、以下の Pull Request がマージされ pkg/errors で wrap されたエラーは Unwrap() error を実装するようになりました。

github.com

これを利用すると pkg/errors で wrap されたエラーであっても Go 1.13 の IsAs が機能するようになります。 例えば今までの pkg/errors だと以下のコードは false を出力しますが、この変更が入っていると true を出力するようになります。

package main

import (
    "errors"
    "fmt"

    pkgerr "github.com/pkg/errors"
)

func main() {
    berr := pkgerr.New("err")
    err := pkgerr.Wrap(berr, "wrapped")
    fmt.Println(errors.Is(err, berr))
}

この変更を適用したい場合、まだリリースタグが切られていないためコミットハッシュを指定して pkg/errors を更新する必要があります。最近の pkg/errors はあまり開発がアクティブではないため、コミットハッシュで固定してもあまり問題にはならないと思います。

2020 年 1 月 13 日追記: v0.9.0 がリリースされました。以下を実行すると更新することができます。

$ go get -u github.com/pkg/errors

これにより、既存コードのエラーの wrap 処理はそのままにしつつ errors.Is や errors.As といった標準ライブラリの機能を使えるようになりました。

pkg/errors にどういった変更が入ったのか

そもそも、pkg/errors にどういった変更が入ったことでこういった振る舞いをするようになったのでしょうか?
Go 1.13 の errors パッケージの IsAs のドキュメントには以下のような記述があります。

The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.

また、 Is の実装を読んでみると、3 つのことを繰り返し行っています。

  1. 比較可能であれば比較する
  2. Is(error) bool を実装していれば呼び出す
  3. Unwrap 関数に err を渡して unwrap する
func Is(err, target error) bool {
    if target == nil {
        return err == target
    }

    isComparable := reflectlite.TypeOf(target).Comparable()
    for {
        if isComparable && err == target {
            return true
        }
        if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
            return true
        }
        // TODO: consider supporting target.Is(err). This would allow
        // user-definable predicates, but also may allow for coping with sloppy
        // APIs, thereby making it easier to get away with them.
        if err = Unwrap(err); err == nil {
            return false
        }
    }
}

Unwrap も非常にシンプルで、ただ型アサーションをしているだけです。

func Unwrap(err error) error {
    u, ok := err.(interface {
        Unwrap() error
    })
    if !ok {
        return nil
    }
    return u.Unwrap()
}

基底のエラーに到達するまで繰り返し同値判定を試みていることがわかります。 ここで重要なのは渡されたエラーが Unwrap() error を実装していればそれが呼び出されるということです。 実際、pkg/errors の Go 1.13 errors に対応した Pull Request の変更も Unwrap() error を実装しただけの非常にシンプルなコードとなっています。

github.com

Go 1.13 errors に対応したライブラリの例

pkg/errors 以外にも Unwrap() errorIs(error) boolAs(interface{}) bool を暗黙的に実装するようになったライブラリがいくつかあります。ここではその一部を紹介します。

標準ライブラリ

当然ですが、標準ライブラリのいくつかのパッケージにも Go 1.13 errors への対応が入っています。
例えば、 net/url パッケージには Error という、発生したエラーに加えて補助的な情報も持つことのできるエラー型がありますが、この型が保持している基底のエラーを Unwrap で取り出せるようになりました。

golang.org

同じように os パッケージの PathErrorLinkErroros/exec パッケージの Error にも Unwrap() error メソッドが追加されています。

cloud.google.com/go/spanner

GCP のライブラリの spanner パッケージは Go 1.13 errors に対応しており *1、返ってくるエラーは Unwrap() error を実装しています。
また、 GRPCStatus() *status.Status というメソッドも実装しているため、以下のように組み合わせることもできます。

var s interface{ GRPCStatus() *status.Status }
if errors.As(spannerErr, &s) {
  fmt.Printf("%v\n", s.GRPCStatus())
}

たとえ pkg/errors で wrap されていたとしてもこのコードは正しく動作します。

まとめ

このように、pkg/errors が使われているコードベースであっても最新のコミットを取り込めば大きな変更なしに Go 1.13 errors に移行できることがわかりました。 pkg/errors は最近はあまりアクティブではなく、Go 1.13 errors でのエラーハンドリングが主流になっていくと思うので移行を検討して観る価値はあると思います。

*1:まだバージョンは切られていませんでした…