🤯

【Go】errors.Isを完全に理解した

2024/12/17に公開

はじめに

Go Advent Calendar 2024 の17日目の記事です。
Goのerrors.Isは、エラーの比較を行うための関数です。
でもこの関数がどういうケースでtrueを返すのか、いまいちよく分かってないという方もいらっしゃるのではないでしょうか?
私もその1人でした。
この記事では、問題とその解答を通して、errors.Isの基本的な挙動を理解することを目指します。
あくまで基本的な挙動の理解を目指しているので、あまり難しい問題ではありません。

まずは問題

以下のQ1-Q7について、errors.Is関数がtrueを返すかどうかを考えてみましょう。

package main

import (
	"errors"
	"fmt"
)

func main() {
	err1 := errors.New(ErrMsg)
	err2 := err1
	fmt.Printf("Q1: %v\n", errors.Is(err1, err2))

	err1 = errors.New(ErrMsg)
	err2 = errors.New(ErrMsg)
	fmt.Printf("Q2: %v\n", errors.Is(err1, err2))

	err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
	err2 = ErrSentinel
	fmt.Printf("Q3: %v\n", errors.Is(err1, err2))

	err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
	err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
	fmt.Printf("Q4: %v\n", errors.Is(err1, err2))

	err1 = ErrSentinel
	err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
	fmt.Printf("Q5: %v\n", errors.Is(err1, err2))

	err1 = &MyError{Msg: ErrMsg}
	err2 = errors.New(ErrMsg)
	fmt.Printf("Q6: %v\n", errors.Is(err1, err2))

	err1 = ErrSentinel
	err2 = &MyError{Msg: ErrMsg}
	fmt.Printf("Q7: %v\n", errors.Is(err1, err2))
}

const ErrMsg = "error message"

var ErrSentinel = errors.New(ErrMsg)

type MyError struct {
	Msg string
}

func (e *MyError) Error() string {
	return e.Msg
}

func (e *MyError) Is(target error) bool {
	return e.Msg == target.Error()
}

version

$ go version
go version go1.23.3 darwin/arm64

解説と解答

先に解答を見たい方は解答へ。

先にまとめ

  • errors.Isの挙動を理解したいなら、errorsパッケージのis関数を見よ
  • is関数では基本的にはポインタが一致してるかだけを見る
  • 基本的にはエラーメッセージの中身を比較するような実装にはなっていない
  • errors.Isの第一引数のerrUnwrapを繰り返され、第二引数とポインタが一致しているかが見られる
  • errors.IsUnwrapを繰り返されるのは第一引数の方であり、第二引数のtargetUnwrapされない
  • errors.Isで第一引数のerrIsメソッドがある場合は、その返り値で評価される
  • errors.IsIsメソッドが評価されるのは第一引数の方であり、第二引数のtargetIsメソッドは評価されない

Q1

    err1 := errors.New(ErrMsg)
    err2 := err1
    fmt.Printf("Q1: %v\n", errors.Is(err1, err2))

errors.Isの挙動を理解するには、errors.Isの実装を見るのが一番早いです。

func Is(err, target error) bool {
	if target == nil {
		return err == target
	}

	isComparable := reflectlite.TypeOf(target).Comparable()
	return is(err, target, isComparable)
}

最初の3行はtargetnilの場合を処理しているだけです。
isComparableは今回のケースでは全てComparable, すなわちtrueを返します。
ということで、is関数が実質的な本体です。

func is(err, target error, targetComparable bool) bool {
	for {
		if targetComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		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 is(err, target, targetComparable) {
					return true
				}
			}
			return false
		default:
			return false
		}
	}
}

forについては後で説明するとして、最初の条件分岐を見ます。
targetComparabletrueなので、基本的にerr == targetが評価されます。
これは同じポインタであればtrueを返すということです。

Q1では以下のように明らかに同じポインタを指しているので、trueが返ります。

    err1 := errors.New(ErrMsg)
    err2 := err1
    fmt.Printf("Q1: %v\n", errors.Is(err1, err2)) // true

Q2

    err1 = errors.New(ErrMsg)
    err2 = errors.New(ErrMsg)
    fmt.Printf("Q2: %v\n", errors.Is(err1, err2))

Q1から分かったように、errors.Isは基本的にはポインタの一致を見ます。
エラーメッセージの中身を比較するような実装にはなっていません。
err1err2は同じエラーメッセージを持っているものの、それぞれerrors.Newで生成されているので、異なるポインタを持っています。
よって、falseが返ります。

    err1 = errors.New(ErrMsg)
    err2 = errors.New(ErrMsg)
    fmt.Printf("Q2: %v\n", errors.Is(err1, err2)) // false

Q3

    err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
    err2 = ErrSentinel
    fmt.Printf("Q3: %v\n", errors.Is(err1, err2))

もう一度errors.isの実装を覗いてみます。

func is(err, target error, targetComparable bool) bool {
	for {
        // 中略
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
        // 中略
		default:
			return false
		}
	}
}

以下の部分は要するに、「errerrorを返すUnwrapメソッドを持っている場合」という意味になります。

		switch x := err.(type) {
		case interface{ Unwrap() error }:

ここでfmt.Errorfの実装を見てみます。

func Errorf(format string, a ...any) error {
	p := newPrinter()
	p.wrapErrs = true
	p.doPrintf(format, a)
	s := string(p.buf)
	var err error
	switch len(p.wrappedErrs) {
	case 0:
		err = errors.New(s)
	case 1:
		w := &wrapError{msg: s}
		w.err, _ = a[p.wrappedErrs[0]].(error)
		err = w
	default:
		if p.reordered {
			slices.Sort(p.wrappedErrs)
		}
		var errs []error
		for i, argNum := range p.wrappedErrs {
			if i > 0 && p.wrappedErrs[i-1] == argNum {
				continue
			}
			if e, ok := a[argNum].(error); ok {
				errs = append(errs, e)
			}
		}
		err = &wrapErrors{s, errs}
	}
	p.free()
	return err
}

色々書いてはありますが、要はfmt.Errorfは第二引数以降が存在していれば&wrapErrorsを返すという意味になります。
このwrapErrorsは以下のように定義されています。

type wrapErrors struct {
	msg  string
	errs []error
}

func (e *wrapErrors) Error() string {
	return e.msg
}

func (e *wrapErrors) Unwrap() []error {
	return e.errs
}

fmt.Errorfで生成されたエラーの実体はwrapErrorsであり、Unwrapメソッドを持っているということになります。
以下の分岐内によって、errorはUnwrapされ、nilでなければ分岐を抜けます。

		switch x := err.(type) {
        case interface{ Unwrap() error }:
            err = x.Unwrap()
            if err == nil {
                return false
            }
}

この処理はfor内にあるため、第一引数のerrUnwrapを繰り返され、第二引数のtargetとポインタが一致しているかを、繰り返し評価されます。
この繰り返しは第一引数のerrUnwrapできなくなるか、Unwrapしたらnilになるまで、続きます。

Q3において、fmt.Errorfで生成されたエラーはUnwrapされて、ErrSentinelが出てきます。
err2は同じくErrSentinelを指しているので、trueが返ります。

    err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
    err2 = ErrSentinel
    fmt.Printf("Q3: %v\n", errors.Is(err1, err2)) // true

Q4

    err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
    err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
    fmt.Printf("Q4: %v\n", errors.Is(err1, err2))

Q3と同様に、fmt.Errorfで生成されたエラーはUnwrapされて、ErrSentinelが出てきます。
しかし、err2はfmt.Errorfで生成されたエラーであり、err1ErrSentinelとは異なるポインタを持っています。
よって、falseが返ります。

    err1 = fmt.Errorf("wrapped: %w", ErrSentinel)
    err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
    fmt.Printf("Q4: %v\n", errors.Is(err1, err2)) // false

Q5

    err1 = ErrSentinel
    err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
    fmt.Printf("Q5: %v\n", errors.Is(err1, err2))

もう一度errors.isの実装を覗いてみます。

func is(err, target error, targetComparable bool) bool {
	for {
        // 中略
		switch x := err.(type) {
		case interface{ Unwrap() error }:
			err = x.Unwrap()
			if err == nil {
				return false
			}
        // 中略
		default:
			return false
		}
	}
}

注意しなくてはいけないのが、Unwrapされるのは第一引数のerrの方であり、第二引数のtargetUnwrapされないということです。

Q5のコードではErrSentinelが第一引数にきており、第二引数であるfmt.Errorfで生成されたエラーはUnwrapされません。
よって、falseが返ります。

    err1 = ErrSentinel
    err2 = fmt.Errorf("wrapped: %w", ErrSentinel)
    fmt.Printf("Q5: %v\n", errors.Is(err1, err2)) // false

Q6

    err1 = &MyError{Msg: ErrMsg}
    err2 = errors.New(ErrMsg)
    fmt.Printf("Q6: %v\n", errors.Is(err1, err2))

もう一度errors.isの実装を覗いてみます。

func is(err, target error, targetComparable bool) bool {
for {
    // 中略
    if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
        return true
    }
	// 中略
}

これは要はboolを返すIsメソッドを持っているか、持っていればそのIsメソッドで評価するという意味です。

MyErrorは以下のように定義されています。

type MyError struct {
    Msg string
}
// 中略
func (e *MyError) Is(target error) bool {
    return e.Msg == target.Error()
}

MyErrorIsメソッドを持っており、その中でエラーメッセージの一致を評価しています。
err1err2はそれぞれ異なるエラーの生成方法で生成されていますが、MyErrorIsで評価され、Is内で評価対象となるエラーメッセージが一致しているため、trueが返ります。

    err1 = &MyError{Msg: ErrMsg}
    err2 = errors.New(ErrMsg)
    fmt.Printf("Q6: %v\n", errors.Is(err1, err2)) // true

Q7

    err1 = ErrSentinel
    err2 = &MyError{Msg: ErrMsg}
    fmt.Printf("Q7: %v\n", errors.Is(err1, err2))

Q5の解説において、以下のような注意を述べました。

注意しなくてはいけないのが、Unwrapされるのは第一引数のerrの方であり、第二引数のtargetUnwrapされないということです。

errors.isをもう一度確認します。
この注意はerr.(interface{ Is(error) bool })の部分に関しても同様です。
すなわち、第一引数のerrIsメソッドだけを見ており、第二引数のtargetIsメソッドは評価されません。

func is(err, target error, targetComparable bool) bool {
for {
    // 中略
    if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
        return true
    }
	// 中略
}

Q7のコードではerr1ErrSentinelであり、これはerrors.Newで生成されたエラーです。
errors.Newで生成されたエラーにはIsメソッドがなく、MyErrorIsメソッドは評価されないため、falseが返ります。

    err1 = ErrSentinel
    err2 = &MyError{Msg: ErrMsg}
    fmt.Printf("Q7: %v\n", errors.Is(err1, err2)) // false

解答

Q1: true
Q2: false
Q3: true
Q4: false
Q5: false
Q6: true
Q7: false

まとめ再掲

  • errors.Isの挙動を理解したいなら、errorsパッケージのis関数を見よ
  • is関数では基本的にはポインタが一致してるかだけを見る
  • 基本的にはエラーメッセージの中身を比較するような実装にはなっていない
  • errors.Isの第一引数のerrUnwrapを繰り返され、第二引数とポインタが一致しているかが見られる
  • errors.IsUnwrapを繰り返されるのは第一引数の方であり、第二引数のtargetUnwrapされない
  • errors.Isで第一引数のerrIsメソッドがある場合は、その返り値で評価される
  • errors.IsIsメソッドが評価されるのは第一引数の方であり、第二引数のtargetIsメソッドは評価されない

Discussion