https://zenn.dev/msksgm/articles/20220325-unwrap-errors-is-as

og-base_z4sxah.png

概要

Go ではデフォルトのエラーに対して、自作エラーを作成して wrap するように有識者の間では推奨されています。 ただ、wrap すると本来のエラーが隠れてしまいテストや実行時に単純な比較ができなくなります。 そこで、よく紹介されるのが、errors.Iserrors.Asです。erros.Iserrors.Asを使うと wrap 済みエラーに対して、元のエラーとの比較がおこなえます。 しかし、紹介記事では、そのままコピー&ペーストするとtrueを返してほしいときに、falseを返してしまう方法を散見します。

本記事では、間違えてしまうパターンについて紹介し、解決方法を紹介します。 元のエラーが不要で、wrap したエラーのみで比較する実装のときには、本記事の手順は不要になります。

上手く比較できないパターン

最初に、上手く比較できないパターンがどのようなときに発生するのか紹介します。

以下は、fmt.Errorfを用いてエラーを wrap した実装です。errors.Iserrors.Asの結果は問題なくtrueを返していることがわかります。

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

func main() {
	if _, err := os.Open("non-existing"); err != nil {
		var pathError *fs.PathError

		wrapedErr := fmt.Errorf("err is %w", err)
		if errors.As(wrapedErr, &pathError) {
			fmt.Println("errors.As():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
		if errors.Is(wrapedErr, pathError) {
			fmt.Println("errors.Is():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
	}
}

// Output
// errors.As():Failed at path: non-existing
// errors.Is():Failed at path: non-existing

続いて、自作エラーで wrap したパターンです。以下の実装ではうまくいきません。 一見、問題なく見えますが、errors.Iserrors.Asfalseを返していることがわかります。 なぜ、このようになるのでしょうか。

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

type SampleError struct {
	message string
	err     error
}

func (se *SampleError) Error() string {
	return se.message
}

func main() {
	if _, err := os.Open("non-existing"); err != nil {
		var pathError *fs.PathError

		wrapedErr := &SampleError{message: "this is wraped err", err: err}
		if errors.As(wrapedErr, &pathError) {
			fmt.Println("errors.As():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
		if errors.Is(wrapedErr, pathError) {
			fmt.Println("errors.Is():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
	}
}

// open non-existing: no such file or directory
// open non-existing: no such file or directory

前述の問題に対する、解決策を紹介します。 タイトル通り、自作エラーにUnwrapを実装する必要があります。

package main

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
)

type SampleError struct {
	message string
	err     error
}

func (se *SampleError) Error() string {
	return se.message
}

func (se *SampleError) Unwrap() error { // 追加
	return se.err
}

func main() {
	if _, err := os.Open("non-existing"); err != nil {
		var pathError *fs.PathError

		wrapedErr := &SampleError{message: "this is wraped err", err: err}
		if errors.As(wrapedErr, &pathError) {
			fmt.Println("errors.As():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
		if errors.Is(wrapedErr, pathError) {
			fmt.Println("errors.Is():Failed at path:", pathError.Path)
		} else {
			fmt.Println(err)
		}
	}
}

// Output:
// errors.As():Failed at path: non-existing
// errors.Is():Failed at path: non-existing

問題に対する解決策は以上になります。 以降は、なぜUnwrap()が必要なのか解説します。

errors.Iserrors.As

この間違いの原因にはerrors.Iserrors.Asの特徴を確認する必要があります。 以下の表に違いをまとめました。 基本的には、エラーの比較方法は 2 あり、false を返す方法は共通であることがわかります。 色々書きましたが、Unwrap できなくなるまで実行されていることが重要です。errors.Iserrors.As の実装の詳細を確認します。

Untitled

errors.Is

以下が、errors.Isの実装です。 表にまとめた通りの実装になっていることがわかります。Unwrapからnilが帰ってきたら、falseを返して終了します。