【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