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

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

このコミットは、Go言語の標準ライブラリ os パッケージにおけるWindows環境でのファイル読み込み処理、特に ReadAt メソッドの挙動に関する修正です。具体的には、ファイルの終端(EOF)に達した際に、Goの io.EOF エラーを正しく返すように変更されています。

変更されたファイルは以下の通りです。

  • src/pkg/os/file_windows.go: Windows環境におけるファイル操作の実装。File.pread メソッドが修正されています。
  • src/pkg/os/os_test.go: os パッケージのテストファイル。TestReadAtEOF という新しいテストケースが追加されています。
  • src/pkg/syscall/ztypes_windows.go: Windowsシステムコールに関連する型定義ファイル。ERROR_HANDLE_EOF の定義が追加されています。

コミット

commit bb6e265fed09754d2a71966f661be9ab084ef43a
Author: Alex Brainman <alex.brainman@gmail.com>
Date:   Mon Jun 10 19:14:41 2013 +1000

    os: return io.EOF from windows ReadAt as documented
    
    Fixes #5619.
    
    R=golang-dev, r, peter.armitage, go.peter.90
    CC=golang-dev
    https://golang.org/cl/9952044

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

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

元コミット内容

os: return io.EOF from windows ReadAt as documented

このコミットは、Windows環境における os パッケージの ReadAt メソッドが、ドキュメントに記載されている通り io.EOF を返すように修正するものです。Issue #5619 を解決します。

変更の背景

Go言語の io パッケージには、データの読み込みがファイルの終端に達したことを示すための標準エラー io.EOF が定義されています。これは、ストリームやファイルからの読み込み操作において、これ以上読み込むデータがないことを呼び出し元に伝えるための重要なシグナルです。

しかし、Windows環境における os パッケージの ReadAt メソッド(内部的には File.pread が使用される)は、ファイルの終端に達した場合に io.EOF を返さず、代わりに読み込んだバイト数が0でエラーが nil となるか、あるいはWindows固有のシステムエラーコードを直接返してしまう可能性がありました。これは、Goの io パッケージのインターフェースの期待される挙動と異なり、クロスプラットフォームでの一貫性を損なう問題でした。

特に、Windows APIの ReadFile 関数は、ファイルの終端に達した場合にエラーを返さず、読み込んだバイト数(lpNumberOfBytesRead)が要求されたバイト数よりも少ない値になることでEOFを示します。しかし、特定の条件下では ERROR_HANDLE_EOF というエラーコードを返すこともあります。Goの os パッケージは、このWindows固有の挙動をGoの標準的な io.EOF に変換する必要がありました。

この不整合は、GoプログラムがWindows上でファイルI/Oを行う際に、EOFの検出ロジックがプラットフォーム間で異なってしまう原因となり、バグや予期せぬ挙動を引き起こす可能性がありました。このコミットは、この問題を修正し、Windows上でも ReadAt が期待通り io.EOF を返すようにすることで、GoのファイルI/Oの堅牢性と移植性を向上させることを目的としています。

前提知識の解説

io.EOF

io.EOF は、Go言語の io パッケージで定義されているエラー変数です。これは、入力ストリームの終端に達し、それ以上データが利用できないことを示すために使用されます。ReadReadAt のような読み込み関数が、要求されたバイト数を読み込む前にストリームの終端に達した場合、読み込んだバイト数と io.EOF エラーを返します。これは、ループでデータを読み込む際に、読み込みを終了する条件としてよく利用されます。

ReadAt メソッド

io.ReaderAt インターフェースの一部であり、os.File が実装しています。ReadAt(b []byte, off int64) (n int, err error) は、指定されたオフセット off からファイルの内容をバイトスライス b に読み込みます。読み込んだバイト数 n とエラー err を返します。このメソッドは、ファイルの現在のオフセットを変更しないという特性があります。

syscall.ReadFile

Windows APIの ReadFile 関数に対応するGoの syscall パッケージの関数です。これは、指定されたファイルハンドルからデータを読み込むための低レベルなシステムコールです。 syscall.ReadFile(handle syscall.Handle, buf []byte, done *uint32, overlapped *syscall.Overlapped) (err error) この関数は、読み込みが成功した場合は nil を返し、エラーが発生した場合は syscall.Errno 型のエラーを返します。読み込んだバイト数は done ポインタを通じて返されます。

syscall.ERROR_HANDLE_EOF

Windows APIで定義されているエラーコードの一つです。このエラーコードは、ファイルハンドルがファイルの終端に達したことを示します。通常、ReadFile 関数はEOFに達してもエラーを返さず、読み込んだバイト数が0になることでEOFを示しますが、特定の状況下(例えば、非同期I/Oやパイプの終端など)でこのエラーが返されることがあります。Goの os パッケージは、このWindows固有のエラーをGoの標準的な io.EOF にマッピングする必要があります。

GoにおけるファイルI/Oの抽象化

Go言語は、異なるオペレーティングシステム(OS)間で一貫したファイルI/Oインターフェースを提供するために、OS固有のシステムコールを抽象化しています。os パッケージは、この抽象化層を提供し、開発者がOSの違いを意識することなくファイル操作を行えるようにします。しかし、この抽象化層の内部では、各OSのシステムコール(Windowsでは syscall パッケージを介してアクセスされる)の挙動をGoの標準インターフェースに適合させるための変換ロジックが必要となります。今回のコミットは、この変換ロジックの改善にあたります。

技術的詳細

このコミットの核心は、Windows環境における os.File.ReadAt メソッドの内部実装である File.pread 関数が、Windows APIの ReadFile から返される可能性のある ERROR_HANDLE_EOF を適切に処理し、Goの io.EOF に変換することです。

従来の File.pread の実装では、syscall.ReadFile がエラーを返した場合、そのエラーをそのまま返していました。しかし、WindowsのファイルI/Oでは、ファイルの終端に達した場合に ReadFileERROR_HANDLE_EOF という特定のエラーコードを返すことがあります。Goの io パッケージの規約では、ファイルの終端に達した場合は io.EOF を返すことになっています。この不一致が問題でした。

コミット前のコードでは、syscall.ReadFileERROR_HANDLE_EOF を返した場合、それはGoの os パッケージのユーザーにとっては予期せぬエラーとして扱われるか、あるいはEOFとして正しく認識されない可能性がありました。

このコミットでは、File.pread 関数内で syscall.ReadFile の戻り値をチェックし、もしエラーが syscall.ERROR_HANDLE_EOF であった場合、読み込んだバイト数 n を0とし、エラーを nil とすることで、呼び出し元が io.EOF を期待するような挙動に近づけています。Goの io.Readerio.ReaderAt の規約では、n < len(b) かつ err == io.EOF の場合にEOFと判断されるため、この修正は io.EOF を直接返しているわけではありませんが、n=0, err=nil のケースを適切に処理することで、io.EOF が返されるべき状況でそれが起こるように間接的に修正しています。

また、syscall/ztypes_windows.goERROR_HANDLE_EOF の定数を追加することで、この特定のエラーコードをGoのコード内でシンボリックに参照できるようにしています。これにより、コードの可読性と保守性が向上します。

さらに、os/os_test.go に追加された TestReadAtEOF テストは、この修正が正しく機能することを確認するためのものです。このテストは、空のファイルに対して ReadAt を呼び出し、その結果が io.EOF であることを検証します。これにより、将来の変更でこの挙動が壊れないように保護されます。

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

src/pkg/os/file_windows.go

--- a/src/pkg/os/file_windows.go
+++ b/src/pkg/os/file_windows.go
@@ -312,6 +312,10 @@ func (f *File) pread(b []byte, off int64) (n int, err error) {
 	var done uint32
 	e = syscall.ReadFile(syscall.Handle(f.fd), b, &done, &o)
 	if e != nil {
+		if e == syscall.ERROR_HANDLE_EOF {
+			// end of file
+			return 0, nil
+		}
 		return 0, e
 	}
 	return int(done), nil

src/pkg/os/os_test.go

--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -1114,3 +1114,19 @@ func TestStatDirModeExec(t *testing.T) {
 		t.Errorf("Stat %q: mode %#o want %#o", path, dir.Mode()&mode, mode)
 	}
 }
+
+func TestReadAtEOF(t *testing.T) {
+	f := newFile("TestReadAtEOF", t)
+	defer Remove(f.Name())
+	defer f.Close()
+
+	_, err := f.ReadAt(make([]byte, 10), 0)
+	switch err {
+	case io.EOF:
+		// all good
+	case nil:
+		t.Fatalf("ReadAt succeeded")
+	default:
+		t.Fatalf("ReadAt failed: %s", err)
+	}
+}

src/pkg/syscall/ztypes_windows.go

--- a/src/pkg/syscall/ztypes_windows.go
+++ b/src/pkg/syscall/ztypes_windows.go
@@ -10,6 +10,7 @@ const (
 	ERROR_PATH_NOT_FOUND      Errno = 3
 	ERROR_ACCESS_DENIED       Errno = 5
 	ERROR_NO_MORE_FILES       Errno = 18
+\tERROR_HANDLE_EOF          Errno = 38
 	ERROR_FILE_EXISTS         Errno = 80
 	ERROR_BROKEN_PIPE         Errno = 109
 	ERROR_BUFFER_OVERFLOW     Errno = 111

コアとなるコードの解説

src/pkg/os/file_windows.go の変更

File.pread 関数は、os.FileReadAt メソッドが内部的に呼び出す関数です。この関数は、Windows APIの ReadFile を使用して実際にファイルからデータを読み込みます。

変更前は、syscall.ReadFile がエラーを返した場合、そのエラーがそのまま呼び出し元に伝播していました。 変更後では、syscall.ReadFile がエラー e を返した場合に、まずそのエラーが syscall.ERROR_HANDLE_EOF であるかどうかをチェックしています。 もし e == syscall.ERROR_HANDLE_EOF であれば、それはファイルの終端に達したことを意味するため、読み込んだバイト数 n0 とし、エラー errnil として返します。 Goの io.Reader および io.ReaderAt の規約では、読み込みバイト数が0でエラーが nil の場合、それはEOFではないことを意味します。しかし、この文脈では、ReadAt が0バイトを読み込み、かつエラーが io.EOF であるべき状況で、Windowsの低レベルAPIが ERROR_HANDLE_EOF を返すという特殊なケースを扱っています。この修正により、ReadAt の呼び出し元は、この特定のWindowsエラーを直接処理する必要がなくなり、Goの標準的なEOF処理に依存できるようになります。

src/pkg/os/os_test.go の変更

TestReadAtEOF という新しいテスト関数が追加されました。 このテストは、まず一時的なファイルを作成し、そのファイルに対して ReadAt を呼び出します。 空のファイルに対して ReadAt(make([]byte, 10), 0) を呼び出すと、ファイルの終端であるため、何も読み込めずに io.EOF エラーが返されることが期待されます。 テストは、ReadAt から返されたエラーが io.EOF であることを switch 文で確認します。 もしエラーが nil であった場合(つまり、ReadAt が成功してしまった場合)、それはテストの失敗とみなされます。 その他のエラーが返された場合も、テストの失敗とみなされます。 このテストの追加により、Windows環境での ReadAt のEOF挙動が正しく修正されたことを検証し、将来のリグレッションを防ぐことができます。

src/pkg/syscall/ztypes_windows.go の変更

このファイルは、Windowsのシステムコールで使用される定数や型を定義しています。 ERROR_HANDLE_EOF という新しい定数が追加されました。その値は 38 です。 この定数の追加により、src/pkg/os/file_windows.go の中で syscall.ERROR_HANDLE_EOF を直接参照できるようになり、マジックナンバー(38)を使用するよりもコードの意図が明確になります。これは、GoのコードがWindows APIのエラーコードをより安全かつ可読性の高い方法で扱うための改善です。

関連リンク

参考にした情報源リンク

  • コミットメッセージと変更されたコード
  • Go言語の io パッケージのドキュメント(io.EOF および io.ReaderAt の規約)
  • Windows APIの ReadFile 関数のドキュメント(ERROR_HANDLE_EOF の挙動)
  • Go言語の syscall パッケージのドキュメント