os.IsNotExist誤用によるファイル存在判定の失敗例

JavaScriptを有効にしてください

前書き

ファイルの存在チェックのためにエラー処理として「誤ったコード例 」のコードを書いていましたが、エラーがラップされるケースで正しく判定できない問題がありました。

具体的には、os.IsNotExist関数を使用して存在チェックを行っていましたが、エラーにスタック情報を付与するためにerrors.WithStackを用いると、元のエラーが隠蔽され、os.IsNotExistでは正しく判定できなくなります。

結論を先に書くと、現在はerrors.Is関数という新しい関数がありこちらを使用します。
じゃあos.IsNotExist関数はいつ使うの?という疑問が湧くと思いますが、こちらはerrors.Is関数が実装される前に使用されていたものです。
今後実装する際にはerrors.Is関数を使用するようにドキュメント にも記載されています。

以下は以前に書いていた誤ったコード例です。

誤ったコード例

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

import (
	"fmt"
	"os"

	"github.com/pkg/errors"
)

func main() {
	readText, err := readTextFile("test.txt")
	// ここで os.IsNotExist を使ってファイルの存在チェックを行うが、
	// errors.WithStack によってエラーがラップされているため正しく判定できない
	if os.IsNotExist(err) {
		fmt.Println("1. ファイルが見つかりません")
		return
	} else if err != nil {
		// 上記以外のエラー
		panic(err)
	}

	fmt.Println(readText)
}

func readTextFile(path string) (string, error) {
	bytes, err := os.ReadFile(path)
	if err != nil {
		// スタック情報を付与しているため、エラーがラップされる
		return "", errors.WithStack(err)
	}
	return string(bytes), nil
}

問題点の解説

  • エラーのラップと判定の問題
    上記コードでは、os.ReadFileで発生したエラーに対してerrors.WithStack(err)を使い、エラーにスタックトレース情報を付与しています。
    この場合、もともとのエラー(たとえば fs.ErrNotExist)はラップされ、os.IsNotExist(err)では直接比較できなくなってしまいます。
    結果、存在しないファイルの場合でもos.IsNotExistfalseを返し、想定外の動作をしてしまいます。

  • 正しい判定方法について
    この問題に対しては、エラーをラップした状態でも元のエラーと比較できるerrors.Is関数を使用する方法が推奨されます。
    もしくは、従来の方法としてはerrors.Cause(err)で元のエラーを取り出してからos.IsNotExistで判定する方法もありましたが、
    errors.Isを使えば一発で判定できるため、よりシンプルです。

以下は、上記誤った例を修正した正しい例です。
エラーのラップ処理後でも、errors.Is関数を使うことで、元のエラー(ここではfs.ErrNotExist)と正しく比較できます。

正しい例

go
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
package main

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

	"github.com/pkg/errors"
)

func main() {
	readText, err := readTextFile("test.txt")
	// errors.Isを使って、ラップされたエラーでも正しく判定できる
	if errors.Is(err, fs.ErrNotExist) {
		fmt.Println("1. ファイルが見つかりません")
		return
	} else if err != nil {
		// 上記以外のエラーはそのままハンドリング
		panic(err)
	}

	fmt.Println(readText)
}

func readTextFile(path string) (string, error) {
	bytes, err := os.ReadFile(path)
	if err != nil {
		// スタック情報を付与してエラーをラップするが、errors.Isなら問題なく比較可能
		return "", errors.WithStack(err)
	}
	return string(bytes), nil
}

解説

  • errors.Is関数
    errors.Is(err, fs.ErrNotExist) を使うことで、エラーがerrors.WithStackでラップされていても、内部に含まれる元のエラーと比較できるため、
    ファイルが存在しない場合に正しく「存在しない」エラーを検出できます。

  • 正しいエラーチェックの利点
    この方法を採用することで、エラーにスタックトレース情報を追加しつつも、エラーチェックが確実に行えるため、
    不測の事態に対する堅牢なエラーハンドリングが実現できます。

このように、ラップされたエラーも含めた比較が必要な場合は、errors.Isを使用することが推奨されます。

まとめ

  • 誤った例では、errors.WithStackでラップされたエラーに対してos.IsNotExistを使ってしまい、
    ファイルが存在しない場合の判定が正しく動作しませんでした。
  • 正しくは、errors.Is(err, fs.ErrNotExist)を使用することで、ラップされたエラーでも正しく判定できます。

この例を通して、エラーのラップ処理とエラー判定の関係に注意しながら実装することが重要だと学んでいただければと思います。

実行環境

cmd
1
2
~/$ go version
go version go1.18 linux/amd64

スポンサーリンク

共有

もふもふ
著者
もふもふ
プログラマ。汎用系→ゲームエンジニア→Webエンジニア→QAエンジニア