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
を実装するようになりました。
これを利用すると pkg/errors で wrap されたエラーであっても Go 1.13 の Is
と As
が機能するようになります。
例えば今までの 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 パッケージの Is
や As
のドキュメントには以下のような記述があります。
The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap.
また、 Is
の実装を読んでみると、3 つのことを繰り返し行っています。
- 比較可能であれば比較する
Is(error) bool
を実装していれば呼び出す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
を実装しただけの非常にシンプルなコードとなっています。
Go 1.13 errors に対応したライブラリの例
pkg/errors 以外にも Unwrap() error
や Is(error) bool
、 As(interface{}) bool
を暗黙的に実装するようになったライブラリがいくつかあります。ここではその一部を紹介します。
標準ライブラリ
当然ですが、標準ライブラリのいくつかのパッケージにも Go 1.13 errors への対応が入っています。
例えば、 net/url
パッケージには Error
という、発生したエラーに加えて補助的な情報も持つことのできるエラー型がありますが、この型が保持している基底のエラーを Unwrap
で取り出せるようになりました。
同じように os
パッケージの PathError
や LinkError
、os/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:まだバージョンは切られていませんでした…