[Golang]`errors.Is()` `errors.As()` 完全ガイド〜使い方と違いをしっかり調査しました〜

はじめに

errors.As()を雰囲気で使っていたらハマったので、errors.Is()も含めて、しっかりと調査してドキュメントとコードを読んだ上でまとめてみました。

マったところ、ハマりそうなところを重点的にまとめてみたので、お役に立てれば幸いです。

何をするメソッドなのか

簡単に

errorを比較してboolを返してくれます。 使いみちとしては、アプリケーションのエラーを外部のエラー(例:gRPCのエラー)に変換したり、ライブラリで使用されているエラーをハンドリングして、アプリケーションのエラーに変換したりするときがあると思います。

errors.Is()

  • 比較対象が保持している値と比較します。
    • 値が同じならtrueを返します。
    • 値が異なるならfalseを返します。
  • interfaceなど比較できない者同士だと必ずfalseになります。

errors.As()

  • 比較対象を型レベルで比較します。
    • 型が同じならtrueを返します。
    • 型が異なるならfalseを返します。
  • 値は違ってもtrueを返します。
  • 引数target(第2引数)には、nilではないポインタ型を渡しましょう。

詳しく

errors.Is()のGoDocとコードを読みました

Is reports whether any error in err’s chain matches target. The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap. An error is considered to match a target if it is equal to that target or if it implements a method Is(error) bool such that Is(target) returns true. An error type might provide an Is method so it can be treated as equivalent to an existing error. https://golang.org/pkg/errors/#Is

そのまま訳すと…

Is() は、errのチェーン内のエラーがターゲットにマッチするかどうかを報告します。 このチェーンはerr自身の後に、Unwrapを繰り返し呼び出すことで得られる一連のエラーで構成されています。 エラーがターゲットと等しい場合、または Is(target)trueを返すような Is(error) bool メソッドを実装している場合、エラーはターゲットと一致しているとみなされます。 エラーの型は、既存のエラーと同等の扱いができるように、Is()メソッドを提供している場合があります。

要するに、errors.Is()は、エラーを比較して同じ値を持っていたらtrue、持っていないならfalseを返してくれます。

注目する点は…

エラーがターゲットと等しい場合

は、Is()の実装は必要無いということです。

もっというと、

エラーの値が比較可能であれば、Is()の実装は必要無いです。 エラーの値が比較不可能であれば、Is()の実装は必要です。

あとは、Wrapしたエラーには使える無いも抑えておくべきです! → Wrapしたエラーと比較するときは、errors.As()を使いましょう。

エラーの値が比較可能なとき

例えば、structError()を実装しerror interfaceを満たして、errorとして扱っている場合のことです。 struct同士は比較可能なので、Is()の実装は必要ありません。

実際のコードを見ると、値を比較できるまでUnwrap()して、比較可能になった時点で、比較していることがわかります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
	isComparable := reflectlite.TypeOf(target).Comparable()
	for {
		if isComparable && err == target {
			return true
		}
		if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
			return true
		}
		// TODO: consider supporting target.Is(err). This would allow
		// user-definable predicates, but also may allow for coping with sloppy
		// APIs, thereby making it easier to get away with them.
		if err = Unwrap(err); err == nil {
			return false
		}
	}

https://github.com/golang/go/blob/master/src/errors/wrap.go#L44-L58

エラーの値が比較 不 可能なとき

例えば、error interfaceを内包したstruct同士を比較したときなどです。 interface同士の比較はできません。

1
isComparable := reflectlite.TypeOf(target).Comparable()

https://github.com/golang/go/blob/master/src/errors/wrap.go#L44

ここにfalseが入るわけです。

下記のサンプルコードでは、interface errorは比較できないので、falseになっています。

1
2
3
4
5
6
7
8
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	is := errors.Is(originalError{err: errors.New("1")}, originalError{err: errors.New("1")})
	fmt.Printf("is = %v because err is not comparable\n", is)
}

https://play.golang.org/p/IN7BHbriu26

なので、Is()を実装して、比較する必要があります。 実装されたIs()では、err.Error()の結果stringを使って比較を行っているので、trueが返ります。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

// implemented!!
func (e originalError) Is(target error) bool { return e.err.Error() == target.Error() }

func main() {
	is := errors.Is(originalError{err: errors.New("1")}, originalError{err: errors.New("1")})
	fmt.Printf("is = %v because originaleError implements Is()\n", is)
}

https://play.golang.org/p/5p8u-D1Hr6q

Wrapしたエラーには使えない

なぜなら、値を比較するのでWrapされた時点で値は比較対象とは異なるはずだからです。 先程も書きましたが、Wrapされたエラーと比較したいならerrors.As()を使いましょう!

ちなみに、エラーをラップするためには標準パッケージを使うと

1
fmt.Errorf("failed to do something: %w", err)

みたいな感じでWrapできます。

errors.As()のGoDocとコードを読みました

As finds the first error in err’s chain that matches target, and if so, sets target to that error value and returns true. Otherwise, it returns false. The chain consists of err itself followed by the sequence of errors obtained by repeatedly calling Unwrap. An error matches target if the error’s concrete value is assignable to the value pointed to by target, or if the error has a method As(interface{}) bool such that As(target) returns true. In the latter case, the As method is responsible for setting target. An error type might provide an As method so it can be treated as if it were a different error type. As panics if target is not a non-nil pointer to either a type that implements error, or to any interface type. https://golang.org/pkg/errors/#As

そのまま訳すと…

As()errのチェインの中で最初のエラーが target にマッチするものを見つけ、マッチしていれば target をそのエラー値に設定してtrueを返します。そうでなければfalseを返します。 チェーンは err 自体の後に、Unwrapを繰り返し呼び出すことで得られる一連のエラーで構成されています。 エラーの具体的な値が target が指す値に代入可能な場合、またはエラーが As(target)trueを返すような As(interface{}) bool メソッドを持っている場合、エラーは target にマッチします。後者の場合は、As()メソッドがtargetの設定を担当します。 エラータイプが As()メソッドを提供している場合は、それが別のエラータイプであるかのように扱うことができます。 As()は、target がエラーを実装した型、または任意のinterfaceへのnilではないポインタである場合にパニックを起こします。

要するに、errors.As()は、エラーを比較して同じ型であればtrue、異なる型ならfalseを返してくれます。

注目する点は…

  • panicになる条件を抑えること。
  • 比較対象の値は同じでは無くて良くて、代入可能であればいいこと。
  • Wrapしたエラーにも使えること。

です。

panicになる条件① 比較対象がnilもしくは、pointerではない型である

1
2
3
4
5
6
7
8
	if target == nil {
		panic("errors: target cannot be nil")
	}
	val := reflectlite.ValueOf(target)
	typ := val.Type()
	if typ.Kind() != reflectlite.Ptr || val.IsNil() {
		panic("errors: target must be a non-nil pointer")
	}

https://github.com/golang/go/blob/master/src/errors/wrap.go#L78-L85

実際にpanicを起こしてみる

比較対象がnilのとき

1
2
3
4
5
6
7
8
9
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	err := &originalError{err: errors.New("1")}
	as := errors.As(err, nil)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/UpCzRpoYPqW

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
./prog.go:14:8: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
Go vet exited.

panic: errors: target cannot be nil

goroutine 1 [running]:
errors.As(0x4deb00, 0xc000010210, 0x0, 0x0, 0xc000032778)
	/usr/local/go-faketime/src/errors/wrap.go:79 +0x5f5
main.main()
	/tmp/sandbox800961272/prog.go:14 +0x9f

比較対象がpointerではない型のとき

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	err := &originalError{err: errors.New("1")}
	var target originalError
	as := errors.As(err, target) // 本当は &target とするべき
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/iF-43pCJf3P

実行結果

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
./prog.go:15:8: second argument to errors.As must be a non-nil pointer to either a type that implements error, or to any interface type
Go vet exited.

panic: errors: target must be a non-nil pointer

goroutine 1 [running]:
errors.As(0x4deae0, 0xc00010a050, 0x4ae1c0, 0xc00010a060, 0xc000068f48)
	/usr/local/go-faketime/src/errors/wrap.go:84 +0x54f
main.main()
	/tmp/sandbox214256996/prog.go:15 +0xd1

panicになる条件② 比較対象がerror interfaceを実装していない

そもそもコンパイルできないので具体例を載せることは割愛しますが、要注意です!

1
2
3
	if e := typ.Elem(); e.Kind() != reflectlite.Interface && !e.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}

https://github.com/golang/go/blob/master/src/errors/wrap.go#L86-L88

panicになる条件」と「比較対象の値は同じでは無くて良くて、代入可能であればいいこと」を踏まえて実装してみる

2つのパターンを用意してみました。

enumを使った実装

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
type ErrorCode uint64

const (
	Zero ErrorCode = iota
	One
)

func (code ErrorCode) Error() string {
	return [...]string{
		"Error: Zero",
		"Error: One",
	}[code]
}
func main() {
	var code ErrorCode
	as := errors.As(Zero, &code)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/Zad83sF-Dxv

structを使った実装

1
2
3
4
5
6
7
8
9
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	var err originalError
	as := errors.As(originalError{err: errors.New("1")}, &err)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/WA-pdwXcM9W

注意点:pointerを意識してください

errors.As()の実装以外でもハマりがちなのが、pointerです。 下記を例にすると、originalError*originalError は違います。 よって、errors.As()falseを返します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	err := originalError{err: errors.New("1")}
	var target *originalError
	as := errors.As(err, &target)
	fmt.Printf("err = %T, target = %T\n", err, target)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/V3WravAWHpb

そして、当たり前なんですが、errの方をpointerにすれば、trueを返します。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
type originalError struct{ err error }

func (e originalError) Error() string { return e.err.Error() }

func main() {
	err := &originalError{err: errors.New("1")} // pointer
	var target *originalError                   // pointer
	as := errors.As(err, &target)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/2RHK2k7ZBJk

Wrapしたエラーとの比較に使えます

As()は値の差異は関係ないので、型が合致しているとtrueを返します。 なので、エラーが持っているメッセージは関係なく、型レベルで同じか確かめたいときに有効です!

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
type ErrorCode uint64

const (
	Zero ErrorCode = iota
	One
)

func (code ErrorCode) Error() string {
	return [...]string{
		"Error: Zero",
		"Error: One",
	}[code]
}
func main() {
	wrappedError := fmt.Errorf("wrap: %w", Zero)
	var code ErrorCode
	as := errors.As(wrappedError, &code)
	fmt.Printf("as = %v\n", as)
}

https://play.golang.org/p/9Sdw-th7znr

As()の第1引数に第2引数の値と型が入ります

第1引数の型と値がポインタで渡した第2引数に代入されます。

注意点は、As()の結果がtruefalseかで挙動が変わるということです。

As()の結果がtrue

エラーがWrapされていても、Wrapされる前の純粋なエラーの型と値が第2引数にそのまま代入されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
type ErrorCode uint

const (
	Zero ErrorCode = iota
	One
)

func (code ErrorCode) Error() string {
	return [...]string{
		"Error: Zero",
		"Error: One",
	}[code]
}

func main() {
	wrappedError := fmt.Errorf("wrap: %w", One)
	var code ErrorCode
	fmt.Printf("before As() code: type %T, value %+v\n", code, code)
	errors.As(wrappedError, &code)
	fmt.Printf("after As() code: type %T, value %+v\n", code, code)
}

出力結果を見ると、値がAs()の後で変わっていること、Wrapされていることは無視されていることがわかります。

1
2
before As() code: type main.ErrorCode, value Error: Zero
after As() code: type main.ErrorCode, value Error: One

https://play.golang.org/p/1opLn8rnq4v

As()の結果がfalse

第1引数の型と値がそのまま第2引数に代入されます。 エラーがWrapされていたらWrapされた後の型と値がそのまま第2引数に代入されます。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
type ErrorCode uint

const (
	Zero ErrorCode = iota
	One
)

func (code ErrorCode) Error() string {
	return [...]string{
		"Error: Zero",
		"Error: One",
	}[code]
}

func main() {
	wrappedError := fmt.Errorf("wrap: %w", One)
	var code error
	errors.As(wrappedError, &code)
	fmt.Printf("code: type %T, value %+v\n", code, code)
}

出力結果を見ると、型と値がWrapされた後のものになっていることがわかります。

1
code: type *fmt.wrapError, value wrap: Error: One

さいごに

結構詳しめにerrors.Is() errors.As()について調べて疲れましたw ただ利用頻度が高いライブラリだと思うので、しっかりと抑えて今日学んだ知識を活かしていきたいと思います。

updatedupdated2020-09-292020-09-29