🐭

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

2020/09/26に公開
1

はじめに

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()して、比較可能になった時点で、比較していることがわかります。

	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同士の比較はできません。

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

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

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

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

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が返ります。

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()を使いましょう!

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

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ではない型である

	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のとき

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

実行結果

./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ではない型のとき

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

実行結果

./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を実装していない

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

	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を使った実装

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を使った実装

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を返します。

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を返します。

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を返します。
なので、エラーが持っているメッセージは関係なく、型レベルで同じか確かめたいときに有効です!

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引数にそのまま代入されます。

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されていることは無視されていることがわかります。

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引数に代入されます。

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された後のものになっていることがわかります。

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

さいごに

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

あと、Zennを初めて使ってみたのですがすごく使いやすくて感動しました。

Discussion

mskmsk

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

記事より引用
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)
}

と書いてある箇所ですが、比較できます。
以下のように、errors.Is()isComparableの式を再現すればわかりますが、trueを返していることがわかります。

https://go.dev/play/p/sB5W28TsRr9

package main

import (
	"errors"
	"fmt"
	"reflect"
)

type originalError struct{ err error }

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

func main() {
	isComparable := reflect.TypeOf(originalError{err: errors.New("1")}).Comparable()
	fmt.Println(isComparable)
}

なぜerrors.Is()falseになるのかというと、errors.New("1") == errors.New("1")falseを返すからです。==reflect.DeepEqualと異なり、Error では同じ値でもfalseを返すからです。
なので、Isを実装するユースケースと若干ずれていることになります。
記事に書かれた string 型の単純比較だと、文字列が変わったときに対応しにくい懸念があります。

https://go.dev/play/p/TDRhUfB5E26

package main

import (
	"errors"
	"fmt"
	"reflect"
)

type originalError struct{ err error }

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

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

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

Wrap したエラーにも使えます。使えない(想定した動作にならない)のはエラーのインスタンスが異なるときです。
以下が、wrapしたIsが比較できる実装になります。
err1のエラーのインスタンスがerr2を Wrap しているので、true になります。

package main

import (
	"errors"
	"fmt"
)

type originalError struct {
	message string
	err     error
}

func (e *originalError) Error() string {
	return e.message
}

func (e *originalError) Unwrap() error {
	return e.err
}

func main() {
	err1 := errors.New("1")
	err2 := &originalError{message: "err2 is wrapped error of err1", err: err1}
	is := errors.Is(err2, err1)
	fmt.Printf("is = %v \n", is)
}

長くなりましたが、errors.Is() について指摘させていただきました。
1年以上前の記事ですが、errorsのソースコードを参考に実行更新してみてください。