【Go】errors.Isを完全に理解した
はじめに
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の第一引数のerrはUnwrapを繰り返され、第二引数とポインタが一致しているかが見られる -
errors.IsでUnwrapを繰り返されるのは第一引数の方であり、第二引数のtargetはUnwrapされない -
errors.Isで第一引数のerrにIsメソッドがある場合は、その返り値で評価される -
errors.IsでIsメソッドが評価されるのは第一引数の方であり、第二引数のtargetのIsメソッドは評価されない
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行はtargetがnilの場合を処理しているだけです。
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については後で説明するとして、最初の条件分岐を見ます。
targetComparableはtrueなので、基本的に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は基本的にはポインタの一致を見ます。
エラーメッセージの中身を比較するような実装にはなっていません。
err1とerr2は同じエラーメッセージを持っているものの、それぞれ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
}
}
}
以下の部分は要するに、「errがerrorを返す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内にあるため、第一引数のerrはUnwrapを繰り返され、第二引数のtargetとポインタが一致しているかを、繰り返し評価されます。
この繰り返しは第一引数のerrがUnwrapできなくなるか、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で生成されたエラーであり、err1やErrSentinelとは異なるポインタを持っています。
よって、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の方であり、第二引数のtargetはUnwrapされないということです。
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()
}
MyErrorはIsメソッドを持っており、その中でエラーメッセージの一致を評価しています。
err1、err2はそれぞれ異なるエラーの生成方法で生成されていますが、MyErrorのIsで評価され、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の方であり、第二引数のtargetはUnwrapされないということです。
errors.isをもう一度確認します。
この注意はerr.(interface{ Is(error) bool })の部分に関しても同様です。
すなわち、第一引数のerrのIsメソッドだけを見ており、第二引数のtargetのIsメソッドは評価されません。
func is(err, target error, targetComparable bool) bool {
for {
// 中略
if x, ok := err.(interface{ Is(error) bool }); ok && x.Is(target) {
return true
}
// 中略
}
Q7のコードではerr1がErrSentinelであり、これはerrors.Newで生成されたエラーです。
errors.Newで生成されたエラーにはIsメソッドがなく、MyErrorのIsメソッドは評価されないため、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の第一引数のerrはUnwrapを繰り返され、第二引数とポインタが一致しているかが見られる -
errors.IsでUnwrapを繰り返されるのは第一引数の方であり、第二引数のtargetはUnwrapされない -
errors.Isで第一引数のerrにIsメソッドがある場合は、その返り値で評価される -
errors.IsでIsメソッドが評価されるのは第一引数の方であり、第二引数のtargetのIsメソッドは評価されない
Discussion