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

# はじめに
こんにちは！エンジニアのKennieです！

自分の所属しているチームのプロダクトで[errors](https://pkg.go.dev/errors)(Goに標準で備わっているエラー処理のためのパッケージ)を使っている箇所と[github.com/pkg/errors](https://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()`で辿れない例：

https://go.dev/play/p/FBX8SKvFP8W
    
    
```go
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()`で辿れる例：

https://go.dev/play/p/4NctawUdpyX


```go
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()`メソッドは提供していません。

```go
// 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()`メソッドを使って元のエラーを取得します。

```go
// 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()`メソッドを使うことで、ラップされたエラーの原因を辿ることができます。

```go

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()`メソッドを持たないエラーに到達すると、それ以上遡ることはできません。

```go
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を使うことが推奨されます！

# 参考
https://github.com/pkg/errors

https://pkg.go.dev/errors

https://go.dev/doc/go1.13#error_wrapping