はじめに
こんにちは!エンジニアのKennieです!
自分の所属しているチームのプロダクトでerrors(Goに標準で備わっているエラー処理のためのパッケージ)を使っている箇所とgithub.com/pkg/errors(現在では開発が停止されてpublic archiveとなっているパッケージ)を使用している箇所がありました。
この2つのパッケージが混在するコードで、fmt.Errorf()
を使ってエラーをラップした場合に、pkg/errorsのCause()
でラップされる前の元のエラーを取得できないことがわかりました。こちらの記事ではラップされていない状態のエラーを「元のエラー」と記載しています。
エラーパッケージの互換性の確認
最初は、「パッケージが異なると元のエラーまで辿ることはできないのでは?」と考えましたが、調べてみると
-
fmt.Errorf("%w", err)
(標準 errors)でラップしたエラーはpkg/errors.Cause()
では元のエラーまで辿れない(新しいパッケージでラップしたエラーを、古いパッケージの仕組みで辿るのはNG) -
pkg/errors.Wrap()
でラップしたエラーはerrors.As()
を使えば元のエラーまで辿れる(古いパッケージでラップしたエラーを、新しいパッケージの仕組みで辿ることはOK)
fmt.Errorf()
でラップしたエラーをpkg/errors.Cause()
で辿れない例:
package main
import (
"fmt"
"github.com/pkg/errors"
)
type MyError struct {
Message string
}
// MyError型のError()メソッドを実装
func (e *MyError) Error() string {
return e.Message
}
func main() {
// MyError型のエラーを作成
originalErr := &MyError{Message: "this is my error"}
// fmt.Errorf()でエラーをラップ
wrappedErr := fmt.Errorf("wrapped error: %w", originalErr)
// pkg/errors.Cause()で元のエラーを取得しようとするが失敗する
// fmt.Errorf()でラップされたエラーにはCause()メソッドがないため、errors.Cause()は元のエラーを取り出せず、ラップが付いた状態で出力される
unwrappedErr := errors.Cause(wrappedErr)
fmt.Println(unwrappedErr)
}
出力
wrapped error: this is my error
pkg/errors.Wrap()
でラップしたエラーをerrors.As()
で辿れる例:
package main
import (
"errors"
"fmt"
// Goの標準パッケージのerrorsと名前が重複するため、github.com/pkg/errorsをlegacyerrorsという別名でインポート
legacyerrors "github.com/pkg/errors"
)
type MyError struct {
Message string
}
// MyError型のError()メソッドを実装
func (e *MyError) Error() string {
return e.Message
}
func main() {
// MyError型のエラーを作成
originalErr := &MyError{Message: "this is my error"}
// github.com/pkg/errorsの.Wrap()でエラーをラップ
wrappedErr := legacyerrors.Wrap(originalErr, "wrapped error")
// errors.As()で元のエラーを取得
var myErr *MyError
// ここで、wrappedErrがMyError型のエラーかどうかチェックし、一致しているのでmyErrに代入される
errors.As(wrappedErr, &myErr)
fmt.Println(myErr.Message)
}
出力
this is my error
この違いから、Goの標準パッケージのエラーハンドリングを使うことで、異なるパッケージ間でもエラーチェーンを辿れることがわかりました。そこで、なぜこのような挙動の違いが生じるのか、それぞれのパッケージの実装を詳しく見ていきましょう。
fmt.Errorf()
でラップされたエラーはpkg/errors.Cause()
では元のエラーを辿れない理由
fmt.Errorf()
は%w
指定子を使用した場合のみエラーメッセージをラップします。その際にUnwrap()
メソッドを持ちますが、Cause()
メソッドは持たないため、causer
インターフェースを実装しておらず、pkg/errors.Cause()
を使って元のエラーを辿ることができません。
fmt.Errorf()
とerrors.Cause()
の構造を見てみましょう。
fmt.Errorf()
によるエラーラップの構造
fmt.Errorf()
がラップするエラーは、内部でwrapError構造体に格納されます。
wrapError
はUnwrap()
メソッドを提供しますが、Cause()
メソッドは提供していません。
// wrapError型はUnwrap()メソッドを持つが、Cause()メソッドは持たない
type wrapError struct {
msg string
err error
}
func (e *wrapError) Error() string {
return e.msg
}
func (e *wrapError) Unwrap() error {
return e.err
}
pkg/errors.Cause()
によるエラーの取得方法
pkg/errors.Cause()
は、エラーがcauser
インターフェース(Cause()
メソッドを持つインターフェース)を実装しているかを確認します。実装していることを確認できれば、Cause()
メソッドを使って元のエラーを取得します。
// pkg/errors.Cause()は、エラーがCause()メソッドを持っている場合に、元のエラーを取り出すことができる
func Cause(err error) error {
type causer interface {
Cause() error
}
for err != nil {
// エラーがcauserインターフェースを実装しているか確認
cause, ok := err.(causer)
if !ok {
break
}
// causerインターフェースを実装していれば`Cause()`メソッドを使用する
err = cause.Cause()
}
return err
}
このように、pkg/errors.Cause()
はcauser
インターフェースに依存しているため、fmt.Errorf()
でラップされたエラーの元を辿ることができません。
pkg/errors.Wrap()
はerrors.As()
で元のエラーまで辿れる理由
errors.As()
は、エラーがUnwrap()
メソッドを持っているかを確認します。Unwrap()
メソッドが実装されていれば、errors.As()
はそれを繰り返し呼び出してエラーチェーンを遡り、元の原因となるエラーにアクセスすることが可能になります。
pkg/errors.Wrap()
は元々Cause()
メソッドを使ってエラーの原因を辿る設計でした。しかし、Go1.13で標準 errors
パッケージにUnwrap()
メソッドが導入されたため、github.com/pkg/errors
側も pkg/errors.Wrap()
が返すエラーにUnwrap()
メソッドを実装しました。
errors.Wrap()
とerrors.As()
の構造を見てみましょう。
errors.Wrap()
によるエラーラップの構造
pkg/errors.Wrap()
が返すエラーには、withMessage構造体が含まれています。この構造体のcauseフィールドには元のエラーが格納されており、Unwrap()
メソッドやCause()
メソッドを使うことで、ラップされたエラーの原因を辿ることができます。
type withMessage struct {
cause error // 元のエラーが格納されている
msg string
}
func (w *withMessage) Error() string {
return w.msg + ": " + w.cause.Error()
}
// Cause()メソッドはwithMessage構造体内のcauseフィールド(元のエラー)を返す
func (w *withMessage) Cause() error {
return w.cause
}
// Unwrap()メソッドはGo 1.13以降追加されました
// Unwrap()メソッドもwithMessage構造体内のcauseフィールド(元のエラー)を返す
func (w *withMessage) Unwrap() error {
return w.cause
}
errors.As()
によるエラーの取得方法
errors.As()
は、エラーがUnwrap()
メソッドを実装しているかを確認し、実装されていれば case interface{ Unwrap() error }
またはcase interface{ Unwrap() []error }
のケースに入り、元のエラーまで辿ります。Unwrap()
メソッドを持たないエラーに到達すると、それ以上遡ることはできません。
switch x := err.(type) {
// 単一のエラーならこちらに入る
case interface{ Unwrap() error }:
err = x.Unwrap()
if err == nil {
return false
}
// 複数のエラーならこちらに入る
case interface{ Unwrap() []error }:
for _, err := range x.Unwrap() {
if err == nil {
continue
}
if as(err, target, targetVal, targetType) {
return true
}
}
return false
// Unwrap()のメソッドを保持していなければ、エラーを辿れないためfalseとなる
default:
return false
}
pkg/errors.Wrap()
が返すエラーは、Unwrap()
メソッドを実装しているため、errors.As()
を使用して元のエラーを辿ることができます。
おわりに
チームのプロダクトでgithub.com/pkg/errorsとerrorsパッケージが混在しているため、互換性を考慮しながら実装をする必要がありました!今回はそれぞれパッケージのコードを見比べて、理解を深めることができました!現在はgithub.com/pkg/errorsはpublic archiveとなりメンテナンスがされない状況になっているので、新規プロジェクトを始める際は、Goの標準パッケージのerrorsを使うことが推奨されます!
参考