⚠️

Golang: errors.Is、errors.Asについて

2023/05/28に公開

こんにちは。

Go言語でテストコードを書いていた時、エラー内容をテストしたい時がありました。その際に、エラーを確認する方法としてerrors.Is、error.Asのどちらを利用すればよいか迷うという経験をしたので、そんな過去の自分に向けて記事を書きたいと思います。

errors.Isについて

まずerrors.Isがどういったものかを説明します。

関数シグネチャは次の通りです。

func Is(err, target error) bool

errors.Isの説明として公式ガイドの1行目には次のように書かれています。

Is reports whether any error in err's tree matches target.

日本語に訳すと、

Isはerrツリー内のerrorがtargetに一致するか否かを報告する。

となります。

言葉だけでは分からないので実装部分も見てみましょう。

errors.Isの実装コード

errors.Isの実装は以下の通りです。

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

	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
		}
	}
}

思っていたほど難しくはなさそうです。1つずつ読み解いていきましょう。

まず、

if target == nil {
	return err == target
}

ここでは、targetがnilの場合に、errとtargetが一致しているかを確かめています。つまり、errもtargetもnilの場合は、errors.Isではtrueが返されるということが分かりました。

次の

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

ではtargetが比較可能か否かの結果を変数として保持しています。因みにreflectliteパッケージはinternal配下にあるパッケージであるため、我々の書くコードからreflectliteで定義されている関数を呼び出すことはできません。

その次にはforループが定義されており、繰り返し処理を行っていることが分かります。

for {
...
}

ではforループの中身を見ていきましょう。

if isComparable && err == target {
	return true
}

先程のisComparableがtrue、つまりtargetは比較可能であり、かつerrとイコールであれば、trueを返すようになっています。

その次は

if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
	return true
}

という処理が書かれています。

まず

err.(interface{ Is(error) bool })

という部分ですが、これはerrをIs(error) boolメソッドを実装したinterfaceに型アサーションしています。型アサーションに成功した場合は、x.Is(target)がtrueかを見ています。

さらに先に進みましょう。forループ最後の箇所です。

if err = Unwrap(err); err == nil {
	return false
}

何やら新しい関数が出てきました。errをUnwrapして、これ以上Unwrapできない(errがnilになる)とfalseを返しています。

Unwrapの実装を見てみましょう。

func Unwrap(err error) error {
	u, ok := err.(interface {
		Unwrap() error
	})
	if !ok {
		return nil
	}
	return u.Unwrap()
}

極めてシンプルです。Unwrapメソッドを持つinterfaceに型アサーションして、失敗したらnilを返し、成功したらUnwrapする。

上記の処理を繰り返し行います。

errors.Isのまとめ

errors.Isがtrueを返す条件は以下の通りになります。、

  • err == targetが共にnilである
  • targetが比較可能な型である時(isComparableがtrue)に、err == targetである
  • errがIsメソッドを実装している時に、err.Is(target)がtrueである

逆にfalseを返す条件は

  • targetがnil, errがnil以外である
  • Unwrap(err) == nilである(すなわち、これ以上Unwrapできない状態である)

注意点

以下のプログラムを見てください。

sample.go
package main

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

func main() {
	if _, err := os.Open("non-existing"); err != nil {
		if errors.Is(err, fs.ErrNotExist) {
			fmt.Println(err) // open non-existing: The system cannot find the file specified.
			fmt.Println(fs.ErrNotExist) // file does not exist
			fmt.Println("file does not exist") // file does not exist
		} else {
			fmt.Println(err)
		}
	}
}

存在しないファイルをOpenしようとしてエラーが発生しています。errors.Isでfs.ErrNotExistと比較するとtrueになります。

errors.Isでtrueになったので、エラーメッセージが同じになる、もしくはメッセージの一部が含まれるのではと思うのですが、実際はそうではありません。

「errors.Isがtrue = エラーメッセージが同じ」という訳ではないことに注意しておきましょう。

次にerrors.Asについて見ていきましょう。

errors.Asについて

まず、関数シグネチャは次の通りです。

func As(err error, target any) bool

errors.Isの場合、targetの型がboolだったところが、errors.Asではanyになっています。

公式ガイドの説明としては、1行目に次のように書かれています。

As finds the first error in err's tree that matches target, and if one is found, sets target to that error value and returns true. Otherwise, it returns false.

先程のerrors.Isとは違い、boolを返すだけでなく、エラーがマッチした際にtargetにエラーの値をセットするようです。

それでは、errors.Asの実装を見てみましょう。

errors.Asの実装

errors/wrap.go
func As(err error, target any) bool {
	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")
	}
	targetType := typ.Elem()
	if targetType.Kind() != reflectlite.Interface && !targetType.Implements(errorType) {
		panic("errors: *target must be interface or implement error")
	}
	for err != nil {
		if reflectlite.TypeOf(err).AssignableTo(targetType) {
			val.Elem().Set(reflectlite.ValueOf(err))
			return true
		}
		if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
			return true
		}
		err = Unwrap(err)
	}
	return false
}

errors.Isに比べると少し複雑ですね。1つずつ見ていきましょう。

最初に

if target == nil {
	panic("errors: target cannot be nil")
}

とtargetがnilであるとpanicを読んでいます。これはerrors.Asではtargetにエラーの値をセットするため、targetがnilだと値のセットができなくなってしまうからです。

val := reflectlite.ValueOf(target)
typ := val.Type()
if typ.Kind() != reflectlite.Ptr || val.IsNil() {
	panic("errors: target must be a non-nil pointer")
}

reflectileという見慣れないものが出てきています。しかしここの処理の本質は、最後のpanicのメッセージに書いてある通り、targetがnon-nil pointerであることを確かめているということです。

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

ここの処理は、if文の条件をド・モルガンの法則で以下のように書き換えてあげるとわかりやすいです。

!(targetType.Kind() == reflectlite.Interface || targetType.Implements(errorType))

panicのメッセージにも書いてありますが、targetポインタがinterfaceである、もしくは、errorを実装している(メソッドとして定義している)場合のみ後続の処理に進むことを許可しています。

for err != nil {
	...
}

errがnilになるまでループを回しています。ではループの中身を見ていきましょう。

if reflectlite.TypeOf(err).AssignableTo(targetType) {
	val.Elem().Set(reflectlite.ValueOf(err))
	return true
}

ここでは、errの値がtargetに代入可能であるならば、targetにerrの値をセットして、trueをリターンして関数を抜けています。

if x, ok := err.(interface{ As(any) bool }); ok && x.As(target) {
	return true
}

ここでは、errをAs(any) boolメソッドを実装したinterfaceに型アサーションしています。型アサーションが成功するかつ、x.As(target)がtrueであるならば、trueを返して関数を抜けています。

ここにはreturn trueの記述しかしていないので、targetへの代入は行われていないかのように思いますが、targetへの値の代入はx.As(target)の中の処理で行われています。

ループの最後にはerrors.Isと同じように

err = Unwrap(err)

Unwrapをしてerrの皮をむいています。

そして上記forループにてerr == nilとなり、ループを抜けた場合は

return false

としてfalseを返して関数を抜けています。

errors.Asのまとめ

errors.Asはerros.Isと違い、panicを起こす可能性があります。その条件は以下のいずれかです。

  • targetがnilである
  • targetがnon-nil pointerでない
  • targetポインタがインターフェースもしくはerrorをメソッドを実装していない

errors.Asは以下の条件のいずれかを満たす時にtrueを返します。

  • errの値がtargetに代入可能である
  • errがAsメソッドを定義しているかつ、targetを引数に取ったAsメソッドの結果がtrueである

errors.Asは以上の条件を満たさない時にfalseを返す。

まとめ

本記事ではerrors.Is、errors.Asについて解説しました。それぞれの違い、使い方を覚えておくと、何かと便利かと思うので上記内容を参考にしてみてください。

参考文献

Discussion