[インデックス 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) error
path
は現在ウォークしているアイテムのパス、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に関する情報)