KDOC 86: errors.Is()の比較ロジック

この文書のステータス

  • 作成
    • 2024-02-17 貴島
  • レビュー
    • 2024-02-18 貴島

概要

Goのerror.Is()は、2つのエラーが一致するかを判定する関数。

func Is(err, target error) bool {}

エラー周りをいまいち理解してないが、調べるなかでエラーというより「一致(==)」とはなにかを理解していなかった。そのへんについて書く。

ユースケースを見る

まずIs()の使い方を理解する。errors packageのexampleを見る。

if _, err := os.Open("non-existing"); err != nil {
	if errors.Is(err, fs.ErrNotExist) {
		fmt.Println("file does not exist")
	} else {
		fmt.Println(err)
	}
}
file does not exist

なんらかの方法で2つのエラー(err, fs.ErrNotExist)を比較し、結果を返している。

↓ここで、ErrNotExistはエラー変数である。

ErrNotExist   = errNotExist()   // "file does not exist"

↓さらに辿る。

func errNotExist() error   { return oserror.ErrNotExist }

↓ようやく定義にたどり着く。

var (
	ErrInvalid    = errors.New("invalid argument")
	ErrPermission = errors.New("permission denied")
	ErrExist      = errors.New("file already exists")
	ErrNotExist   = errors.New("file does not exist")

errors.New()が返す構造体*errors.errorStringはUnwrap()、Is()を実装していないから、エラーチェーンを辿ることはない。単純にerrors.New()で作られたエラー変数自体を「比較」していることになる。比較ロジックはなんだろうか。実装を見る。

if targetComparable && err == target {

シンプルに == で比較している。ひとくちに比較といってもいろいろある。型の要素が一致しているか、値が一致しているか、指すメモリアドレスが同じか、など。どうなるのだろうか。

比較ロジック

動かして調べる。値や、型名の比較ではないことを確認する。

fmt.Println("↓falseになる")
fmt.Println(errors.Is(fs.ErrNotExist, errors.New("file does not exist")))
fmt.Println("↓==を使ってもfalseになる")
fmt.Println(fs.ErrNotExist == errors.New("file does not exist"))
fmt.Println("↓型は同じなのを確認する")
fmt.Printf("%T, %T\n", fs.ErrNotExist, errors.New("file does not exist"))
fmt.Println("↓フィールドの値は同じ")
fmt.Printf("%#v, %#v\n", fs.ErrNotExist, errors.New("file does not exist"))
↓falseになる
false
↓==を使ってもfalseになる
false
↓型は同じなのを確認する
*errors.errorString, *errors.errorString
↓フィールドの値は同じ
&errors.errorString{s:"file does not exist"}, &errors.errorString{s:"file does not exist"}

ここでよく見るとポインタ型なので、指すメモリアドレスを比較していると考えた。同じ変数だと、同じものを指しているので同じになるだろう。よく忘れるのだが、 error はインターフェースであって、型ではない。よく使われるerrors.New()は*errorString型のインスタンスを作成する。

メモリアドレスの比較であることを確かめる。

// 値が完全に同じでも、メモリアドレスが違うのでfalse
fmt.Println(errors.New("dummy") == errors.New("dummy"))

e := errors.New("dummy")
// メモリアドレスが同じなのでtrue
fmt.Println(e == e)
// メモリアドレスが同じであればいいので別の変数に入れて元の変数と比較してもtrue
ee := e
fmt.Println(ee == e)
false
true
true

errorを実装している型をポインタ型でないものにすると、値で比較するため構造体の値が同じであれば同じ判定になる。現実で使うケースはなさそうだが。

package main

import (
	"errors"
	"fmt"
)

type dummy struct{ s string }

func (d dummy) Error() string { return "" }

var _ error = dummy{}

func main() {
	fmt.Println(errors.Is(dummy{}, dummy{}))       // true
	fmt.Println(errors.Is(dummy{"a"}, dummy{"b"})) // false
}

フィールドの値で比較していることがわかる。

まとめ

  • errors.Is()はエラーチェーンをたどったり独自の判定ロジックを適用してくれるが、もっともシンプルな例だと単純に == で比較しているだけにすぎない
  • error はインターフェースであり、よく見るerrors.New()で作られる変数の型はその実装の1つにすぎない
  • エラーのパッケージ変数はerrors.New()で作られる*errors.errorString型がよく使われる。ポインタなので、パッケージ変数として初期化・公開しておくと一致を安全に確認できる。ポインタ型なので、 型で比較しているわけではない

関連