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

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

このコミットは、Go言語の os パッケージにおける os.FileReaddir メソッドの挙動を修正するものです。具体的には、Readdir がディレクトリ内のエントリの Lstat 呼び出しで発生したエラーをどのように処理するかについて、Go 1.1.2 のセマンティクスを復元することを目的としています。

コミット

commit ef805fe3a06c251903b3d634d1e6dd3d15d3245f
Author: Russ Cox <rsc@golang.org>
Date:   Tue Oct 29 11:50:40 2013 -0400

    os: do not return Lstat errors from Readdir
    
    This CL restores the Go 1.1.2 semantics for os.File's Readdir method.
    
    The code in Go 1.1.2 was rewritten mainly because it looked buggy.
    This new version attempts to be clearer but still provide the 1.1.2 results.
    
    The important diff is not this CL's version against tip but this CL's version
    against Go 1.1.2.
    
    Go 1.1.2:
    
            names, err := f.Readdirnames(n)
            fi = make([]FileInfo, len(names))
            for i, filename := range names {
                    fip, err := Lstat(dirname + filename)
                    if err == nil {
                            fi[i] = fip
                    } else {
                            fi[i] = &fileStat{name: filename}
                    }\n            }\n            return fi, err
    
    This CL:
    
            names, err := f.Readdirnames(n)
            fi = make([]FileInfo, len(names))
            for i, filename := range names {
                    fip, lerr := lstat(dirname + filename)
                    if lerr != nil {
                            fi[i] = &fileStat{name: filename}
                            continue
                    }
                    fi[i] = fip
            }
            return fi, err
    
    The changes from Go 1.1.2 are stylistic, not semantic:
    1. Use lstat instead of Lstat, for testing (done before this CL).
    2. Make error handling in loop body look more like an error case.
    3. Use separate error variable name in loop body, to be clear
       we are not trying to influence the final return result.
    
    Fixes #6656.
    Fixes #6680.
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/18870043

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

https://github.com/golang/go/commit/ef805fe3a06c251903b3d634d1e6dd3d15d3245f

元コミット内容

このコミットは、os.FileReaddir メソッドが、ディレクトリ内の各エントリに対して Lstat を呼び出す際に発生するエラーの処理方法を修正しています。以前のバージョンでは、Lstat がエラーを返した場合、そのエラーが Readdir メソッド全体の戻り値 err に影響を与える可能性がありました。このコミットは、Lstat のエラーを個々の FileInfo エントリに限定し、Readdir メソッド自体は Lstat エラーによって中断されないように、Go 1.1.2 のセマンティクスを復元します。

変更の背景

Go 1.1.2 の os.File.Readdir メソッドのコードは、その後のバージョンで「バグがあるように見えた」という理由で書き直されました。しかし、その書き換えによって、Readdir がディレクトリ内の個々のエントリの Lstat 呼び出しでエラーが発生した場合に、そのエラーを Readdir メソッドの最終的な戻り値 err に伝播させてしまうという、意図しないセマンティクスの変更が生じていました。

ファイルシステムは動的な環境であり、Readdir が実行されている間にファイルが削除されたり、アクセス権が変更されたりすることは珍しくありません。このような状況で Lstat がエラーを返した場合、Go 1.1.2 ではそのエントリの FileInfo はエラー情報を含まないダミーの fileStat オブジェクトで置き換えられ、Readdir メソッド自体は成功として完了し、他の有効なエントリの FileInfo を返していました。しかし、その後の変更では、Lstat エラーが発生すると Readdir メソッド全体がエラーを返す可能性があり、これはユーザーが期待する挙動と異なる場合がありました。

このコミットは、この意図しないセマンティクスの変更を元に戻し、Go 1.1.2 の挙動、すなわち Lstat エラーが発生しても Readdir メソッド自体はエラーを返さず、問題のあるエントリはダミーの FileInfo で表現されるという挙動を復元します。これにより、部分的に読み取り可能なディレクトリであっても、可能な限り多くの情報を取得できるようになります。

コミットメッセージで言及されている Fixes #6656Fixes #6680 は、この変更によって解決されるバグトラッキングシステムの課題番号ですが、Goの公開イシュートラッカーではこれらの番号に直接関連する情報は見つかりませんでした。これは、内部的な課題番号であるか、または非常に古い課題である可能性があります。

前提知識の解説

このコミットを理解するためには、以下のGo言語の概念とファイルシステム操作に関する知識が必要です。

  • os.File.Readdir(n int) ([]FileInfo, error): このメソッドは、ディレクトリの内容を読み取り、最大 n 個の FileInfo オブジェクトのスライスを返します。n が0より大きい場合、最大 n 個のエントリが返されます。n が0以下の場合、すべてのエントリが返されます。このメソッドは、ディレクトリの読み取り中にエラーが発生した場合に error を返します。
  • os.FileInfo インターフェース: このインターフェースは、ファイルに関する情報(名前、サイズ、パーミッション、最終更新時刻など)を提供します。Readdir メソッドは、ディレクトリ内の各エントリに対してこのインターフェースを実装したオブジェクトを返します。
  • os.Lstat(name string) (FileInfo, error): この関数は、指定されたパス name のファイルまたはディレクトリの FileInfo を返します。Stat とは異なり、シンボリックリンクの場合、リンク自体に関する情報を返し、リンクが指すファイルの情報は返しません。ファイルが存在しない、またはアクセスできない場合など、エラーが発生する可能性があります。
  • エラーハンドリング: Go言語では、関数は通常、最後の戻り値として error 型の値を返します。nil はエラーがないことを意味し、非nil の値はエラーが発生したことを示します。このコミットでは、ループ内で発生する Lstat エラーが、外側の Readdir メソッドの最終的な戻り値 err に影響を与えないように、エラー変数のスコープと処理が重要になります。
  • continue キーワード: Go言語の for ループ内で continue キーワードが使用されると、現在のイテレーションの残りの部分がスキップされ、次のイテレーションが開始されます。このコミットでは、Lstat エラーが発生した場合に、そのエントリの処理をスキップし、次のエントリの処理に進むために使用されています。

技術的詳細

このコミットの核心は、os.File.Readdir メソッドの内部実装における Lstat エラーの処理ロジックの変更です。

元のGo 1.1.2のコードは以下のようになっていました。

            names, err := f.Readdirnames(n)
            fi = make([]FileInfo, len(names))
            for i, filename := range names {
                    fip, err := Lstat(dirname + filename) // ここで新しい 'err' 変数が宣言される
                    if err == nil {
                            fi[i] = fip
                    } else {
                            fi[i] = &fileStat{name: filename}
                    }
            }
            return fi, err // ここで返される 'err' は Readdirnames からのもの

このコードでは、for ループ内で fip, err := Lstat(...) とすることで、新しい err 変数が宣言され、Lstat のエラーはその新しい err に格納されます。この err はループのスコープ内でのみ有効であり、Readdirnames から返された外側の err 変数とは別物です。したがって、Lstat がエラーを返しても、外側の err には影響を与えません。エラーが発生した場合、fi[i] にはダミーの fileStat が設定されます。

しかし、Go 1.1.2 以降のバージョンでは、このロジックが変更され、Lstat エラーが Readdir メソッドの最終的な戻り値 err に伝播する可能性がありました。コミットメッセージにはその具体的なコードは示されていませんが、おそらく Lstat エラーが発生した場合に、外側の err 変数に Lstat のエラーを代入するようなロジックが含まれていたと推測されます。

このコミットで導入されたコードは以下のようになります。

            names, err := f.Readdirnames(n)
            fi = make([]FileInfo, len(names))
            for i, filename := range names {
                    fip, lerr := lstat(dirname + filename) // 'lerr' という新しい変数名を使用
                    if lerr != nil { // エラーが発生した場合
                            fi[i] = &fileStat{name: filename} // ダミーの FileInfo を設定
                            continue // 次のイテレーションへ進む
                    }
                    fi[i] = fip // エラーがなければ通常の FileInfo を設定
            }
            return fi, err // Readdirnames からの 'err' を返す

この変更のポイントは以下の通りです。

  1. lstat の使用: Lstat の代わりに lstat が使用されています。コミットメッセージによると、これはテストのために以前に行われた変更であり、このコミットのセマンティクスには影響しません。
  2. エラー変数の明確化: Lstat の戻り値のエラー変数に lerr という新しい名前が付けられています。これにより、このエラーが Readdir メソッドの最終的な戻り値 err とは別のものであることが明確になります。
  3. エラー処理の明確化: if lerr != nil という条件でエラーケースを明示的に処理しています。
  4. continue の使用: Lstat がエラーを返した場合、fi[i] にダミーの fileStat を設定した後、continue を使用してループの次のイテレーションに直接進みます。これにより、Lstat エラーが Readdir メソッドの最終的な戻り値 err に影響を与えることなく、ディレクトリの読み取り処理が続行されます。

結果として、この変更により、Readdir メソッドは Readdirnames からのエラー(ディレクトリ自体を読み取れない場合など)のみを返し、個々の Lstat 呼び出しで発生したエラーは、そのエントリの FileInfo をダミーで置き換えることで吸収されるようになります。これは、Go 1.1.2 の挙動と一致し、部分的にアクセスできないディレクトリでも Readdir が成功する可能性を高めます。

テストファイル src/pkg/os/os_unix_test.go の変更も重要です。TestReaddirWithBadLstat 関数は、Lstat がエラーを返す状況をシミュレートし、Readdir が期待通りにエラーを返さないことを検証しています。

変更前:

 	if err != ErrInvalid {
 		t.Fatalf("Expected Readdir to return ErrInvalid, got %v", err)
 	}

変更後:

 	if err != nil {
 		t.Fatalf("Expected Readdir to return no error, got %v", err)
 	}

このテストの変更は、ReaddirLstat エラーを返さないという新しい(復元された)セマンティクスを反映しています。以前は ErrInvalid を期待していましたが、変更後は nil エラーを期待するようになっています。

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

src/pkg/os/file_unix.goreaddir 関数内のループ部分が変更されています。

--- a/src/pkg/os/file_unix.go
+++ b/src/pkg/os/file_unix.go
@@ -165,14 +165,11 @@ func (f *File) readdir(n int) (fi []FileInfo, err error) {
 	fi = make([]FileInfo, len(names))\n 	for i, filename := range names {
 	\tfip, lerr := lstat(dirname + filename)
-\t\tif lerr == nil {
-\t\t\tfi[i] = fip
-\t\t} else {
+\t\tif lerr != nil {
 \t\t\tfi[i] = &fileStat{name: filename}
-\t\t\tif err == nil {
-\t\t\t\terr = lerr
-\t\t\t}\n+\t\t\tcontinue
 \t\t}
+\t\tfi[i] = fip
 \t}
 \treturn fi, err
 }

また、src/pkg/os/os_unix_test.goTestReaddirWithBadLstat 関数も変更されています。

--- a/src/pkg/os/os_unix_test.go
+++ b/src/pkg/os/os_unix_test.go
@@ -92,8 +92,8 @@ func TestReaddirWithBadLstat(t *testing.T) {
 	defer func() { *LstatP = Lstat }()
 
 	dirs, err := handle.Readdir(-1)
-\tif err != ErrInvalid {
-\t\tt.Fatalf("Expected Readdir to return ErrInvalid, got %v", err)
+\tif err != nil {
+\t\tt.Fatalf("Expected Readdir to return no error, got %v", err)
 	}
 	foundfail := false
 	for _, dir := range dirs {

コアとなるコードの解説

src/pkg/os/file_unix.go の変更は、readdir 関数(Readdir メソッドの実体)内で、ディレクトリ内の各エントリに対して lstat を呼び出すループのロジックを修正しています。

変更前は、lerr == nil の場合に fi[i] = fip を実行し、else ブロックで lerr != nil の場合に fi[i] = &fileStat{name: filename} を設定していました。さらに、lerrnil でない場合に、外側の err 変数に lerr を代入しようとするロジック(if err == nil { err = lerr })がありました。このロジックは、Readdirnames からのエラーがまだ err に設定されていない場合にのみ lerrerr に伝播させることを意図していた可能性がありますが、Go 1.1.2 のセマンティクスとは異なっていました。

変更後は、if lerr != nil という条件でエラーケースを直接処理します。エラーが発生した場合、fi[i] にはファイル名のみを持つダミーの fileStat オブジェクトが設定され、continue キーワードによってループの次のイテレーションにスキップされます。これにより、lstat エラーが readdir 関数の最終的な戻り値 err に影響を与えることがなくなります。最終的に readdir 関数は、Readdirnames から返された err をそのまま返します。

src/pkg/os/os_unix_test.go の変更は、このセマンティクスの変更をテストに反映させたものです。TestReaddirWithBadLstat は、Lstat がエラーを返すようにモックされた状況で Readdir を呼び出します。変更前は、ReaddirErrInvalid を返すことを期待していましたが、これは Lstat エラーが伝播する挙動をテストしていました。変更後は、Readdirnil エラーを返すことを期待するようになり、これは Lstat エラーが吸収される新しい(復元された)セマンティクスを正確にテストしています。

関連リンク

コミットメッセージで言及されている Fixes #6656 および Fixes #6680 については、Goの公開イシュートラッカーでは直接関連する情報を見つけることができませんでした。

参考にした情報源リンク

  • コミットメッセージ自体
  • Go言語の公式ドキュメント (pkg.go.dev)
  • Go言語のソースコード (GitHub)