Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 13249] ファイルの概要

このコミットは、Go言語の標準ライブラリ path/filepath パッケージにおける Walk 関数の SkipDir エラーの挙動を修正し、ドキュメントに記載されている通りの動作を保証するものです。具体的には、WalkFuncfilepath.SkipDir を返した場合に、そのディレクトリの走査をスキップするという約束が守られていなかったバグ(Issue #3486)を修正しています。

コミット

commit 2b57a87678caa3adebc3254b1a54d18ab2ada941
Author: Jan Mercl <befelemepeseveze@gmail.com>
Date:   Sat Jun 2 13:00:09 2012 -0400

    path/filepath: implement documented SkipDir behavior
    
    Currently walk() doesn't check for err == SkipDir when iterating
    a directory list, but such promise is made in the docs for WalkFunc.
    
    Fixes #3486.
    
    R=rsc, r
    CC=golang-dev
    https://golang.org/cl/6257059

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/2b57a87678caa3adebc3254b1a54d18ab2ada941

元コミット内容

path/filepath: ドキュメント化された SkipDir の挙動を実装

現在、walk() はディレクトリリストをイテレートする際に err == SkipDir をチェックしていませんが、WalkFunc のドキュメントにはそのような約束がされています。

Issue #3486 を修正します。

変更の背景

Go言語の path/filepath パッケージには、ファイルシステムツリーを走査するための Walk 関数が存在します。この関数は、走査中に各ファイルやディレクトリに対してユーザー定義の WalkFunc を呼び出します。WalkFunc のドキュメントには、もし関数が filepath.SkipDir エラーを返した場合、Walk はそのディレクトリの内容を走査しない(スキップする)という明確な記述がありました。

しかし、実際の walk 関数の内部実装では、この SkipDir の挙動が正しく処理されていませんでした。具体的には、walk 関数が子ディレクトリを再帰的に走査する際に、WalkFunc から返されたエラーが SkipDir であったとしても、そのエラーを適切に解釈せず、単にエラーとして上位に伝播させてしまっていました。これにより、ユーザーが特定のディレクトリをスキップしようとしても、期待通りに動作せず、場合によっては予期せぬエラーとして処理が中断される可能性がありました。

この不整合は、GoのIssueトラッカーで #3486 として報告されました。このコミットは、この報告されたバグを修正し、filepath.Walk のドキュメントに記載されている SkipDir のセマンティクスを正確に実装することを目的としています。

前提知識の解説

path/filepath パッケージ

path/filepath パッケージは、Go言語の標準ライブラリの一部であり、ファイルパスの操作、特にオペレーティングシステムに依存しないパス操作を提供します。これには、パスの結合、クリーンアップ、相対パスと絶対パスの変換、そしてファイルシステムツリーの走査などが含まれます。

filepath.Walk 関数

filepath.Walk は、指定されたルートパスから始まるファイルシステムツリーを再帰的に走査するための関数です。そのシグネチャは以下の通りです。

func Walk(root string, walkFn WalkFunc) error
  • root: 走査を開始するディレクトリのパス。
  • walkFn: WalkFunc 型の関数で、走査中に見つかった各ファイルやディレクトリに対して呼び出されます。

filepath.WalkFunc

WalkFuncfilepath.Walk 関数に渡されるコールバック関数の型定義です。

type WalkFunc func(path string, info os.FileInfo, err error) error
  • path: 現在走査中のファイルまたはディレクトリのパス。
  • info: os.FileInfo インターフェースで、ファイルまたはディレクトリのメタデータ(名前、サイズ、パーミッション、変更時刻など)を提供します。
  • err: Walk 関数が path を走査する際に発生したエラー。もしエラーが nil でない場合、infonil になる可能性があります。

WalkFunc が返すエラーによって、Walk 関数の挙動を制御できます。

filepath.SkipDir エラー

filepath.SkipDir は、WalkFunc が返すことができる特別なエラー値です。WalkFuncfilepath.SkipDir を返した場合、Walk 関数は現在のディレクトリの内容(サブディレクトリやファイル)の走査をスキップし、そのディレクトリの兄弟要素(同じ親ディレクトリ内の他のファイルやディレクトリ)の走査に進みます。これは、特定のディレクトリツリーを無視して走査の効率を向上させたい場合や、アクセス権がないディレクトリでエラーを発生させずにスキップしたい場合などに非常に有用です。

os.FileInfo インターフェース

os.FileInfo は、ファイルまたはディレクトリに関する情報を抽象化するインターフェースです。IsDir() メソッドは、それがディレクトリであるかどうかを判断するために使用されます。

type FileInfo interface {
    Name() string       // base name of the file
    Size() int64        // length in bytes for regular files; system-dependent for others
    Mode() FileMode     // file mode bits
    ModTime() time.Time // modification time
    IsDir() bool        // abbreviation for Mode().IsDir()
    Sys() interface{}   // underlying data source (can return nil)
}

技術的詳細

このコミットの核心は、filepath.Walk の内部で呼び出される walk 関数(非公開関数)のロジック変更にあります。変更前は、walk 関数が子要素を再帰的に走査する際に、WalkFunc から返されたエラーが nil でない限り、無条件にそのエラーを上位に伝播させていました。

変更後のコードでは、walk 関数が子要素の走査からエラーを受け取った際に、以下の条件分岐を追加しています。

  1. エラーが nil でない場合:
    • 走査対象がディレクトリであり (fileInfo.IsDir()true)、かつ
    • 返されたエラーが filepath.SkipDir である場合 (err == SkipDir) この場合は、エラーを無視して次の兄弟要素の走査に進みます。つまり、SkipDir が意図通りにディレクトリのスキップとして機能します。
  2. 上記以外のケース(エラーが SkipDir でない、または走査対象がファイルであるにもかかわらず SkipDir が返されたなど)では、エラーはそのまま上位に伝播されます。

この修正により、WalkFuncSkipDir を返した場合に、そのディレクトリの内部が走査されなくなり、filepath.Walk のドキュメントに記載されている通りの挙動が保証されるようになりました。

また、この修正を検証するために、path_test.go に新しいテストケース TestBug3486 が追加されました。このテストは、filepath.Walk を使用して GOROOT を走査し、特定のディレクトリ (lib) で filepath.SkipDir を返すことで、そのディレクトリがスキップされ、しかし他の重要なディレクトリ (src) は正しく走査されることを確認します。これにより、SkipDir の挙動が期待通りであることを自動的に検証できるようになりました。

コアとなるコードの変更箇所

src/pkg/path/filepath/path.go

--- a/src/pkg/path/filepath/path.go
+++ b/src/pkg/path/filepath/path.go
@@ -320,8 +320,11 @@ func walk(path string, info os.FileInfo, walkFn WalkFunc) error {
 	}
 
 	for _, fileInfo := range list {
-		if err = walk(Join(path, fileInfo.Name()), fileInfo, walkFn); err != nil {
-			return err
+		err = walk(Join(path, fileInfo.Name()), fileInfo, walkFn)
+		if err != nil {
+			if !fileInfo.IsDir() || err != SkipDir {
+				return err
+			}
 		}
 	}
 	return nil

src/pkg/path/filepath/path_test.go

--- a/src/pkg/path/filepath/path_test.go
+++ b/src/pkg/path/filepath/path_test.go
@@ -874,3 +874,26 @@ func TestDriveLetterInEvalSymlinks(t *testing.T) {
 		t.Errorf("Results of EvalSymlinks do not match: %q and %q", flp, fup)
 	}\n
 }\n
+\n
+func TestBug3486(t *testing.T) { // http://code.google.com/p/go/issues/detail?id=3486
+\troot := os.Getenv("GOROOT")
+\tlib := filepath.Join(root, "lib")
+\tsrc := filepath.Join(root, "src")
+\tseenSrc := false
+\tfilepath.Walk(root, func(pth string, info os.FileInfo, err error) error {
+\t\tif err != nil {
+\t\t\tt.Fatal(err)
+\t\t}
+\n
+\t\tswitch pth {\n
+\t\tcase lib:\n
+\t\t\treturn filepath.SkipDir
+\t\tcase src:\n
+\t\t\tseenSrc = true
+\t\t}\n
+\t\treturn nil
+\t})\n
+\tif !seenSrc {\n
+\t\tt.Fatalf("%q not seen", src)
+\t}\n
+}\n

コアとなるコードの解説

src/pkg/path/filepath/path.go の変更

変更の中心は、walk 関数内のループ処理です。

変更前:

		if err = walk(Join(path, fileInfo.Name()), fileInfo, walkFn); err != nil {
			return err
		}

ここでは、子要素の walk 呼び出しからエラーが返された場合、それがどのようなエラーであっても無条件に return err していました。

変更後:

		err = walk(Join(path, fileInfo.Name()), fileInfo, walkFn)
		if err != nil {
			if !fileInfo.IsDir() || err != SkipDir {
				return err
			}
		}

この修正では、まず err = walk(...) で子要素の走査を行い、その結果を err 変数に格納します。 次に if err != nil でエラーが発生したかどうかをチェックします。 エラーが発生した場合、さらにネストされた if 文で条件を評価します。 !fileInfo.IsDir() || err != SkipDir この条件は以下のいずれかの状況で true になります。

  1. !fileInfo.IsDir()true の場合: つまり、走査対象がディレクトリではない(ファイルである)場合。ファイルに対して SkipDir が返されることは通常想定されず、これは予期せぬエラーとして処理されるべきです。
  2. err != SkipDirtrue の場合: つまり、エラーが SkipDir ではない場合。これは、ファイルシステムアクセスエラーなど、走査を中断すべき真のエラーであるため、上位に伝播させる必要があります。

この条件が true の場合のみ return err が実行され、エラーが上位に伝播されます。 逆に、この条件が false の場合(つまり、fileInfo.IsDir()true かつ err == SkipDir の場合)、return err は実行されず、ループは次の fileInfo(兄弟要素)の処理に進みます。これにより、SkipDir が返されたディレクトリの内部走査がスキップされるという、ドキュメント通りの挙動が実現されます。

src/pkg/path/filepath/path_test.go の追加

TestBug3486 は、この修正が正しく機能することを確認するためのテストです。

  1. root := os.Getenv("GOROOT"): Goのインストールルートディレクトリを取得します。
  2. lib := filepath.Join(root, "lib"): GOROOT/lib パスを構築します。このディレクトリはテストでスキップされることを期待します。
  3. src := filepath.Join(root, "src"): GOROOT/src パスを構築します。このディレクトリはスキップされずに走査されることを期待します。
  4. seenSrc := false: GOROOT/src が走査されたかどうかを追跡するためのフラグです。
  5. filepath.Walk(root, func(...) error { ... }): GOROOT をルートとして Walk 関数を呼び出します。
  6. switch pth { ... }: WalkFunc 内で、現在のパス pth に応じて挙動を制御します。
    • case lib:: もしパスが GOROOT/lib であれば、filepath.SkipDir を返します。これにより、lib ディレクトリの内容が走査されないことを期待します。
    • case src:: もしパスが GOROOT/src であれば、seenSrc フラグを true に設定します。これは、src ディレクトリが正しく走査されたことを示すものです。
  7. if !seenSrc { t.Fatalf(...) }: Walk 関数が完了した後、seenSrcfalse のままであれば、GOROOT/src が走査されなかったことになり、テストは失敗します。これは、SkipDir が意図しないディレクトリまでスキップしてしまった場合に検出するためのアサーションです。

このテストは、SkipDir が特定のディレクトリのみをスキップし、それ以外のディレクトリは期待通りに走査されることを保証します。

関連リンク

参考にした情報源リンク

  • 上記の関連リンクに記載されているGoの公式ドキュメントとIssueトラッカー。
  • Go言語のソースコード。
  • filepath.Walk の挙動に関する一般的なGoプログラミングの知識。
  • os.FileInfo インターフェースに関する一般的なGoプログラミングの知識。
  • Goのテストフレームワーク testing パッケージに関する一般的な知識。