[インデックス 18020] ファイルの概要
このコミットは、Go言語の標準ライブラリであるosパッケージとpath/filepathパッケージにおける、ディレクトリ読み取り(Readdir)およびファイルウォーク(Walk)時のエラーハンドリングの改善を目的としています。特に、Lstat(シンボリックリンクを辿らないstatシステムコール)が返すエラーの扱いを修正し、path/filepath.WalkがWalkFuncの契約(各ウォークされたアイテムのLstatエラーを返す)を遵守するように変更されました。これにより、ファイルシステムの状態が動的に変化する状況(例: Readdir中にファイルが削除される)や、Lstatが予期せぬエラーを返す場合に、より堅牢で予測可能な動作が保証されます。
コミット
- コミットハッシュ:
6a1a2170bcd1fbbe7210d90939a485dadf5075fb - Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Tue Dec 17 12:19:01 2013 -0800
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/6a1a2170bcd1fbbe7210d90939a485dadf5075fb
元コミット内容
os, path/filepath: don't ignore Lstat errors in Readdir
os: don't ignore LStat errors in Readdir. If it's ENOENT,
on the second pass, just treat it as missing. If it's another
error, it's real.
path/filepath: use ReaddirNames instead of Readdir in Walk,
in order to obey the documented WalkFunc contract of returning
each walked item's LStat error, if any.
Fixes #6656
Fixes #6680
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/43530043
変更の背景
このコミットは、os.Readdirとpath/filepath.Walkの既存の動作における、Lstatエラーの不適切な無視と、WalkFuncの契約不履行という2つの主要な問題を解決するために行われました。
-
os.ReaddirにおけるLstatエラーの無視: 以前のos.Readdirの実装では、ディレクトリ内の各エントリに対してLstatを呼び出してファイル情報を取得していました。しかし、このLstat呼び出しがエラーを返した場合、特にファイルが存在しないことを示すENOENT(またはos.ErrNotExist)エラーの場合でも、そのエラーが適切に処理されず、単にそのエントリのFileInfoがデフォルト値で埋められるか、あるいはエラーが無視されていました。これにより、ディレクトリの内容が動的に変化する環境(例:Readdirの呼び出し中にファイルが削除される)で、不正確なファイル情報が返されたり、予期せぬ動作が発生する可能性がありました。コミットメッセージにある「on the second pass, just treat it as missing. If it's another error, it's real.」という記述は、Readdirがディレクトリの内容を一度読み取った後、各エントリのLstatを再度試みる際に、ENOENTであればファイルが消えたと判断し、それ以外のエラーであれば真のエラーとして扱うべきだという方針を示しています。 -
path/filepath.WalkにおけるWalkFunc契約の不履行:path/filepath.Walk関数は、指定されたディレクトリツリーを再帰的に辿り、見つかった各ファイルやディレクトリに対してユーザー定義のWalkFuncを呼び出します。WalkFuncの契約では、ウォークされたアイテムのos.FileInfoと、そのアイテムのLstat呼び出しで発生したエラー(もしあれば)を引数として受け取ることが期待されています。しかし、以前の実装では、Walkが内部でos.Readdirを使用しており、このReaddirがLstatエラーを適切に伝播しなかったため、WalkFuncが常に正確なLstatエラーを受け取ることができませんでした。このコミットでは、Readdirの代わりにReaddirNamesを使用し、Walk自身が各エントリに対してLstatを呼び出し、そのエラーをWalkFuncに渡すように変更することで、この契約不履行を修正しています。
これらの問題は、それぞれGoのIssue #6656と #6680で報告されていた可能性がありますが、現在の検索ではこれらのIssueの詳細は見つかりませんでした。しかし、コミットメッセージから、これらの変更がGoのファイルシステム操作の堅牢性と正確性を向上させるために不可欠であったことがわかります。
前提知識の解説
このコミットを理解するためには、以下のGo言語の標準ライブラリの概念とファイルシステム操作に関する知識が必要です。
-
osパッケージ: Go言語でオペレーティングシステムと対話するための基本的な機能を提供するパッケージです。ファイル操作、ディレクトリ操作、プロセス管理などが含まれます。os.File: 開かれたファイルまたはディレクトリを表す型です。(*os.File).Readdir(n int) ([]os.FileInfo, error): ディレクトリの内容を読み取り、os.FileInfoのスライスを返します。nが0より大きい場合、最大n個のFileInfoを返します。nが0以下の場合、すべてのFileInfoを返します。このメソッドは、ディレクトリ内の各エントリに対してLstatを内部的に呼び出し、そのファイル情報を取得します。(*os.File).Readdirnames(n int) ([]string, error): ディレクトリの内容を読み取り、エントリ名のスライスを返します。Readdirとは異なり、FileInfoは返さず、名前のみを返します。このメソッドはLstatを内部的に呼び出しません。os.Lstat(name string) (os.FileInfo, error): 指定されたパスのファイル情報を返します。os.Statとは異なり、シンボリックリンクを辿りません。つまり、パスがシンボリックリンクである場合、リンク自体の情報を返します。ファイルが存在しない場合、os.ErrNotExistエラーを返します。os.FileInfoインターフェース: ファイルのメタデータ(名前、サイズ、パーミッション、最終更新時刻など)を提供するインターフェースです。os.IsNotExist(err error) bool: エラーがファイルまたはディレクトリが存在しないことを示すエラー(os.ErrNotExist)である場合にtrueを返します。これは、syscall.ENOENTなどのシステムコールエラーをラップしている可能性があります。
-
path/filepathパッケージ: ファイルパスを操作するためのユーティリティ関数を提供するパッケージです。プラットフォーム固有のパス区切り文字などを考慮して、クロスプラットフォームなパス操作を可能にします。path/filepath.Walk(root string, walkFn WalkFunc) error: 指定されたrootパスから始まるディレクトリツリーを再帰的に辿ります。見つかった各ファイルまたはディレクトリに対して、ユーザー定義のWalkFuncを呼び出します。path/filepath.WalkFunc型:Walk関数に渡されるコールバック関数の型です。type WalkFunc func(path string, info os.FileInfo, err error) errorpathは現在ウォークしているアイテムのパス、infoはそのアイテムのos.FileInfo、errはそのアイテムのLstat呼び出しで発生したエラー(もしあれば)です。WalkFuncがfilepath.SkipDirを返すと、そのディレクトリはスキップされます。それ以外のエラーを返すと、Walkは停止し、そのエラーを返します。
-
ENOENT(Error No ENTry): Unix系システムにおけるエラーコードの一つで、"No such file or directory"(そのようなファイルやディレクトリはない)を意味します。Go言語では、os.ErrNotExistとして抽象化されています。
技術的詳細
このコミットの技術的詳細は、os.Readdirとpath/filepath.Walkの内部動作の変更に集約されます。
os.Readdirの変更点
以前のos.Readdirの実装では、ディレクトリからエントリ名を読み取った後、各エントリに対してLstatを呼び出してFileInfoを取得していました。この際、Lstatがエラーを返しても、そのエラーが適切に処理されず、特にos.ErrNotExist(ファイルが読み取り中に消滅した場合など)が無視される傾向がありました。
変更後のos.Readdir(具体的にはFile.readdirメソッド)は、以下のロジックでLstatエラーを処理します。
- ディレクトリからエントリ名(
names)のリストを取得します。 - 各
filenameに対してlstat(dirname + filename)を呼び出します。 lerr(Lstatが返したエラー)がos.IsNotExist(lerr)である場合、そのファイルはreaddirとstatの間に消滅したと判断し、単にそのエントリをスキップします(continue)。これは、一時的なファイルや動的に生成・削除されるファイルシステムエントリを扱う際に、不必要なエラーを発生させないための堅牢なアプローチです。lerrがos.IsNotExistではないが、nilでもない場合(つまり、真のLstatエラーが発生した場合)、そのエラーを直ちに呼び出し元に返します。これにより、ファイルシステムの問題(例: パーミッションエラー、I/Oエラーなど)が適切に報告されるようになります。lerrがnilの場合、取得したFileInfoを結果のスライスに追加します。
この変更により、os.Readdirは、ファイルが一時的に存在しない状況を許容しつつ、その他の重要なLstatエラーは確実に報告するようになりました。
path/filepath.Walkの変更点
以前のpath/filepath.Walkは、内部でreadDirというヘルパー関数を使用していました。このreadDirはos.Readdirを呼び出し、os.FileInfoのスライスを返していました。しかし、前述のos.Readdirの問題により、WalkFuncに渡されるFileInfoやエラーが不正確になる可能性がありました。
変更後のpath/filepath.Walk(具体的にはwalk関数)は、以下の重要な変更が加えられました。
readDirNamesの使用:walk関数は、ディレクトリ内のエントリ名を取得するために、readDirの代わりに新しく導入されたreadDirNames関数を使用するようになりました。readDirNamesはos.Readdirnamesを呼び出すため、Lstatを内部的に実行せず、単にファイル名のリストを返します。これにより、Walk関数が各エントリのLstatを明示的に制御できるようになります。Lstatの明示的な呼び出しとエラーハンドリング:walk関数は、readDirNamesで取得した各nameに対して、filename := Join(path, name)で完全なパスを構築し、fileInfo, err := lstat(filename)を呼び出してFileInfoを取得するようになりました。WalkFuncへのエラー伝播:lstat呼び出しでエラー(err)が発生した場合、walkFn(filename, fileInfo, err)を呼び出し、このlstatエラーをWalkFuncに直接渡します。これにより、WalkFuncの契約が遵守され、ユーザーは各アイテムのLstatエラーを正確に処理できるようになります。WalkFuncがnilまたはfilepath.SkipDir以外のエラーを返した場合、Walkは停止し、そのエラーを返します。lstat呼び出しが成功した場合(errがnil)、通常の再帰的なwalk処理(walk(filename, fileInfo, walkFn))を続行します。ここでも、再帰的なwalk呼び出しがエラーを返した場合、WalkFuncの契約に従ってエラーを処理します。
これらの変更により、path/filepath.Walkは、各ファイルやディレクトリのLstatエラーを正確にWalkFuncに報告するようになり、より予測可能で堅牢なファイルウォーク機能を提供します。また、テストのためにos.Lstatを置き換えられるように、filepath.LstatPというエクスポートされた変数も追加されています。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルとコード箇所は以下の通りです。
-
src/pkg/os/file_unix.go:(*File).readdirメソッド内で、lstatエラーの処理ロジックが変更されました。fi = make([]FileInfo, len(names))がfi = make([]FileInfo, 0, len(names))に変更され、appendでFileInfoを追加するようになりました。lerr != nilのチェックがIsNotExist(lerr)とlerr != nilの2段階に分けられました。
-
src/pkg/os/os_test.go:TestReaddirStatFailuresという新しいテストが追加され、ReaddirがLstatエラーを適切に処理するかどうかを検証しています。特に、os.ErrNotExistの場合とそれ以外のエラーの場合の動作を確認しています。
-
src/pkg/os/os_unix_test.go:TestReaddirWithBadLstatという古いテストが削除されました。これは、os.Readdirの変更により不要になったか、新しいテストでカバーされるようになったためと考えられます。
-
src/pkg/path/filepath/export_test.go:- 新しく追加されたファイルで、
filepathパッケージの内部変数lstatをテストからアクセスできるようにLstatPとしてエクスポートしています。これにより、テストでos.Lstatの動作をモックできるようになります。
- 新しく追加されたファイルで、
-
src/pkg/path/filepath/path.go:walk関数内で、readDirの代わりにreadDirNamesが使用されるようになりました。walk関数内で、各ファイルエントリに対して明示的にlstatが呼び出され、そのエラーがWalkFuncに渡されるロジックが追加されました。readDir関数が削除され、代わりにreadDirNames関数が追加されました。readDirNamesはos.Readdirnamesを呼び出し、ファイル名のソートされたリストを返します。byName型とそれに関連するsort.Interfaceの実装が削除されました。これは、readDirがFileInfoをソートして返していたのに対し、readDirNamesは名前のみを返し、sort.Stringsでソートするためです。
-
src/pkg/path/filepath/path_test.go:TestWalkFileErrorという新しいテストが追加され、filepath.WalkがLstatエラーをWalkFuncに適切に伝播するかどうかを検証しています。
コアとなるコードの解説
src/pkg/os/file_unix.go の変更
// 変更前 (抜粋)
func (f *File) readdir(n int) (fi []FileInfo, err error) {
// ...
fi = make([]FileInfo, len(names))
for i, filename := range names {
fip, lerr := lstat(dirname + filename)
if lerr != nil {
fi[i] = &fileStat{name: filename} // エラーを無視し、デフォルトのFileInfoを設定
continue
}
fi[i] = fip
}
return fi, err
}
// 変更後 (抜粋)
func (f *File) readdir(n int) (fi []FileInfo, err error) {
// ...
fi = make([]FileInfo, 0, len(names)) // 容量のみ確保し、appendで追加
for _, filename := range names {
fip, lerr := lstat(dirname + filename)
if IsNotExist(lerr) { // ファイルが存在しないエラーの場合
// File disappeared between readdir + stat.
// Just treat it as if it didn't exist.
continue // スキップ
}
if lerr != nil { // その他のエラーの場合
return fi, lerr // 直ちにエラーを返す
}
fi = append(fi, fip) // 正常なFileInfoを追加
}
return fi, err
}
この変更により、os.ReaddirはLstatエラーをより厳密に扱うようになりました。特に、ファイルが一時的に存在しない(ENOENT)場合はスキップし、それ以外のエラーは即座に報告することで、ファイルシステム操作の堅牢性が向上しています。
src/pkg/path/filepath/path.go の変更
// 変更前 (抜粋)
func walk(path string, info os.FileInfo, walkFn WalkFunc) error {
// ...
list, err := readDir(path) // readDirはos.Readdirを呼び出す
if err != nil {
return walkFn(path, info, err)
}
for _, fileInfo := range list {
err = walk(Join(path, fileInfo.Name()), fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != SkipDir {
return err
}
}
}
return nil
}
// 変更後 (抜粋)
var lstat = os.Lstat // for testing
func walk(path string, info os.FileInfo, walkFn WalkFunc) error {
// ...
names, err := readDirNames(path) // readDirNamesはos.Readdirnamesを呼び出す
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := Join(path, name)
fileInfo, err := lstat(filename) // 各エントリに対して明示的にlstatを呼び出す
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
return err
}
} else {
err = walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != SkipDir {
return err
}
}
}
}
return nil
}
// 変更前 (readDir関数)
// func readDir(dirname string) ([]os.FileInfo, error) { /* ... */ }
// 変更後 (readDirNames関数)
func readDirNames(dirname string) ([]string, error) {
f, err := os.Open(dirname)
if err != nil {
return nil, err
}
names, err := f.Readdirnames(-1) // os.Readdirnamesを使用
f.Close()
if err != nil {
return nil, err
}
sort.Strings(names) // 名前をソート
return names, nil
}
path/filepath.Walkの変更は、WalkFuncの契約を遵守するために非常に重要です。os.Readdirnamesを使用してファイル名のみを取得し、その後Walk自身が各ファイルに対してLstatを呼び出すことで、Lstatで発生したエラーを正確にWalkFuncに伝播できるようになりました。これにより、ユーザーはWalkFunc内でファイルシステムエラーを適切に処理できるようになります。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/6a1a2170bcd1fbbe7210d90939a485dadf5075fb
- Go CL (Code Review): https://golang.org/cl/43530043
- 関連するGo Issue (検索では詳細が見つかりませんでした): #6656, #6680
参考にした情報源リンク
- Go言語のコミット情報:
/home/orange/Project/comemo/commit_data/18020.txt - Go言語の公式ドキュメント(
osパッケージ、path/filepathパッケージ) - Go言語のソースコード(コミットの差分)
- Web検索(Go言語のIssueに関する情報)