7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Goのエラーパッケージ間の互換性とエラーチェーンの取り扱い

Last updated at Posted at 2025-03-31

はじめに

こんにちは!エンジニアの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構造体に格納されます。
wrapErrorUnwrap()メソッドを提供しますが、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を使うことが推奨されます!

参考

7
4
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
7
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?