[インデックス 13249] ファイルの概要
このコミットは、Go言語の標準ライブラリ path/filepath
パッケージにおける Walk
関数の SkipDir
エラーの挙動を修正し、ドキュメントに記載されている通りの動作を保証するものです。具体的には、WalkFunc
が filepath.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
型
WalkFunc
は filepath.Walk
関数に渡されるコールバック関数の型定義です。
type WalkFunc func(path string, info os.FileInfo, err error) error
path
: 現在走査中のファイルまたはディレクトリのパス。info
:os.FileInfo
インターフェースで、ファイルまたはディレクトリのメタデータ(名前、サイズ、パーミッション、変更時刻など)を提供します。err
:Walk
関数がpath
を走査する際に発生したエラー。もしエラーがnil
でない場合、info
はnil
になる可能性があります。
WalkFunc
が返すエラーによって、Walk
関数の挙動を制御できます。
filepath.SkipDir
エラー
filepath.SkipDir
は、WalkFunc
が返すことができる特別なエラー値です。WalkFunc
が filepath.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
関数が子要素の走査からエラーを受け取った際に、以下の条件分岐を追加しています。
- エラーが
nil
でない場合:- 走査対象がディレクトリであり (
fileInfo.IsDir()
がtrue
)、かつ - 返されたエラーが
filepath.SkipDir
である場合 (err == SkipDir
) この場合は、エラーを無視して次の兄弟要素の走査に進みます。つまり、SkipDir
が意図通りにディレクトリのスキップとして機能します。
- 走査対象がディレクトリであり (
- 上記以外のケース(エラーが
SkipDir
でない、または走査対象がファイルであるにもかかわらずSkipDir
が返されたなど)では、エラーはそのまま上位に伝播されます。
この修正により、WalkFunc
が SkipDir
を返した場合に、そのディレクトリの内部が走査されなくなり、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
になります。
!fileInfo.IsDir()
がtrue
の場合: つまり、走査対象がディレクトリではない(ファイルである)場合。ファイルに対してSkipDir
が返されることは通常想定されず、これは予期せぬエラーとして処理されるべきです。err != SkipDir
がtrue
の場合: つまり、エラーがSkipDir
ではない場合。これは、ファイルシステムアクセスエラーなど、走査を中断すべき真のエラーであるため、上位に伝播させる必要があります。
この条件が true
の場合のみ return err
が実行され、エラーが上位に伝播されます。
逆に、この条件が false
の場合(つまり、fileInfo.IsDir()
が true
かつ err == SkipDir
の場合)、return err
は実行されず、ループは次の fileInfo
(兄弟要素)の処理に進みます。これにより、SkipDir
が返されたディレクトリの内部走査がスキップされるという、ドキュメント通りの挙動が実現されます。
src/pkg/path/filepath/path_test.go
の追加
TestBug3486
は、この修正が正しく機能することを確認するためのテストです。
root := os.Getenv("GOROOT")
: Goのインストールルートディレクトリを取得します。lib := filepath.Join(root, "lib")
:GOROOT/lib
パスを構築します。このディレクトリはテストでスキップされることを期待します。src := filepath.Join(root, "src")
:GOROOT/src
パスを構築します。このディレクトリはスキップされずに走査されることを期待します。seenSrc := false
:GOROOT/src
が走査されたかどうかを追跡するためのフラグです。filepath.Walk(root, func(...) error { ... })
:GOROOT
をルートとしてWalk
関数を呼び出します。switch pth { ... }
:WalkFunc
内で、現在のパスpth
に応じて挙動を制御します。case lib:
: もしパスがGOROOT/lib
であれば、filepath.SkipDir
を返します。これにより、lib
ディレクトリの内容が走査されないことを期待します。case src:
: もしパスがGOROOT/src
であれば、seenSrc
フラグをtrue
に設定します。これは、src
ディレクトリが正しく走査されたことを示すものです。
if !seenSrc { t.Fatalf(...) }
:Walk
関数が完了した後、seenSrc
がfalse
のままであれば、GOROOT/src
が走査されなかったことになり、テストは失敗します。これは、SkipDir
が意図しないディレクトリまでスキップしてしまった場合に検出するためのアサーションです。
このテストは、SkipDir
が特定のディレクトリのみをスキップし、それ以外のディレクトリは期待通りに走査されることを保証します。
関連リンク
- Go Issue 3486: https://code.google.com/p/go/issues/detail?id=3486
- Go CL 6257059: https://golang.org/cl/6257059
- Go
path/filepath
パッケージドキュメント: https://pkg.go.dev/path/filepath - Go
filepath.Walk
ドキュメント: https://pkg.go.dev/path/filepath#Walk - Go
filepath.SkipDir
ドキュメント: https://pkg.go.dev/path/filepath#SkipDir
参考にした情報源リンク
- 上記の関連リンクに記載されているGoの公式ドキュメントとIssueトラッカー。
- Go言語のソースコード。
filepath.Walk
の挙動に関する一般的なGoプログラミングの知識。os.FileInfo
インターフェースに関する一般的なGoプログラミングの知識。- Goのテストフレームワーク
testing
パッケージに関する一般的な知識。