[インデックス 10163] ファイルの概要
このコミットは、Go言語のos
パッケージにおけるファイル読み込みの挙動、特にRead
メソッドがゼロ長(0バイト)の読み込み要求を受けた際の振る舞いを修正するものです。以前のバージョンでは、ゼロ長の読み込みが即座にファイルの終端(EOF)として解釈される可能性がありましたが、このコミットにより、それが修正され、より正確なファイル読み込みのセマンティクスが保証されるようになりました。
コミット
commit 4853c51770f5e99d5d690801e5cb963848591587
Author: Russ Cox <rsc@golang.org>
Date: Tue Nov 1 00:17:05 2011 -0400
os: do not interpret 0-length read as EOF
Fixes #2402.
R=golang-dev, bradfitz, r
CC=golang-dev
https://golang.org/cl/5298081
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/4853c51770f5e99d5d690801e5cb963848591587
元コミット内容
このコミットの元のメッセージは「os: do not interpret 0-length read as EOF」であり、ゼロ長の読み込みをEOFとして解釈しないようにするという明確な目的を示しています。これは、Go言語のos
パッケージにおけるFile.Read
メソッドの挙動に関するバグ(Issue 2402)を修正するためのものです。
変更の背景
Go言語のio.Reader
インターフェースは、Read(p []byte) (n int, err error)
というシグネチャを持ちます。このメソッドは、p
に最大len(p)
バイトを読み込み、読み込んだバイト数n
とエラーerr
を返します。重要なのは、n
が0であっても、それが必ずしもEOFを意味するわけではないという点です。例えば、読み込むデータが一時的に利用できない場合(非ブロッキングI/Oなど)、またはp
がゼロ長のバイトスライスである場合、n
は0になる可能性がありますが、ファイルが終端に達したわけではありません。
しかし、Goの初期の実装では、os.File.Read
メソッドがn == 0
かつエラーがない場合に、無条件にEOF
を返してしまうという問題がありました。これは、特にRead(make([]byte, 0))
のようにゼロ長のバイトスライスを渡してRead
を呼び出した場合に問題となります。このような呼び出しは、通常、ファイルの終端に達したかどうかを確認するためではなく、単に読み込み操作をトリガーしたり、現在の読み込み位置を確認したりするために使用されることがあります。しかし、このバグにより、ファイルがまだ終端に達していないにもかかわらず、EOF
が返されてしまい、アプリケーションのロジックが誤動作する可能性がありました。
この問題は、GoのIssue 2402として報告され、このコミットによって修正されました。
前提知識の解説
io.Reader
インターフェースとRead
メソッド
Go言語におけるio.Reader
インターフェースは、データを読み込むための基本的な抽象化を提供します。多くのI/O操作(ファイル、ネットワーク接続、メモリバッファなど)がこのインターフェースを実装しています。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドの戻り値n
とerr
には以下の重要なセマンティクスがあります。
n > 0
: 少なくとも1バイトが読み込まれたことを示します。err
はnil
であるか、読み込み中に発生した非致命的なエラー(例: 一部のバイトが読み込まれた後にネットワーク接続が切断された場合など)を示すことがあります。n == 0
かつerr == nil
: これは、Read
がブロックせずに0バイトを読み込んだことを意味します。これは、データが一時的に利用できない場合(非ブロッキングI/O)や、p
がゼロ長のバイトスライスである場合に発生します。これはEOFを意味しません。n == 0
かつerr == io.EOF
: これは、ファイルの終端に達し、それ以上読み込むデータがないことを明確に示します。n == 0
かつerr != nil
(io.EOF
以外): 読み込み中にエラーが発生し、1バイトも読み込めなかったことを示します。
このコミットの修正は、特に2番目のケース(n == 0
かつerr == nil
)において、誤ってio.EOF
を返してしまう問題を解決することを目的としています。
EOF
(End Of File)
EOF
は、ファイルやストリームの終端に達したことを示す特別なエラー値です。Goのio
パッケージではio.EOF
として定義されています。Read
メソッドがio.EOF
を返す場合、それはそれ以上読み込むデータがないことを意味します。
os.File
os.File
は、Go言語でファイルシステム上のファイルを表す型です。Read
メソッドを含むio.Reader
インターフェースを実装しており、ファイルの読み込み操作を提供します。
技術的詳細
このコミットの核心は、src/pkg/os/file.go
内のFile.Read
メソッドの条件式にlen(b) > 0
という条件を追加したことです。
変更前のコード:
if n == 0 && !iserror(e) {
return 0, EOF
}
変更後のコード:
if n == 0 && len(b) > 0 && !iserror(e) {
return 0, EOF
}
この変更により、Read
メソッドがn == 0
(読み込んだバイト数が0)かつiserror(e)
がfalse
(システムコールからのエラーがない)の場合でも、さらにlen(b) > 0
(読み込みバッファb
の長さが0より大きい)という条件が満たされない限り、EOF
を返さないようになりました。
つまり、Read(b []byte)
が呼び出され、b
がゼロ長のバイトスライス(len(b) == 0
)である場合、たとえシステムコールが0を返し、エラーがなかったとしても、EOF
は返されなくなります。これは、io.Reader
のセマンティクスに合致し、ゼロ長の読み込みがEOFを意味しないという原則を遵守します。
また、この修正を検証するために、src/pkg/os/os_test.go
にTestRead0
という新しいテストケースが追加されました。このテストは、ゼロ長のバイトスライスをRead
メソッドに渡した場合の挙動と、その後に通常の長さのバイトスライスを渡した場合の挙動を検証します。
コアとなるコードの変更箇所
src/pkg/os/file.go
--- a/src/pkg/os/file.go
+++ b/src/pkg/os/file.go
@@ -69,7 +69,7 @@ func (file *File) Read(b []byte) (n int, err Error) {
if n < 0 {
n = 0
}
- if n == 0 && !iserror(e) {
+ if n == 0 && len(b) > 0 && !iserror(e) {
return 0, EOF
}
if iserror(e) {
src/pkg/os/os_test.go
--- a/src/pkg/os/os_test.go
+++ b/src/pkg/os/os_test.go
@@ -165,6 +165,27 @@ func TestLstat(t *testing.T) {
}
}
+// Read with length 0 should not return EOF.
+func TestRead0(t *testing.T) {
+ path := sfdir + "/" + sfname
+ f, err := Open(path)
+ if err != nil {
+ t.Fatal("open failed:", err)
+ }
+ defer f.Close()
+
+ b := make([]byte, 0)
+ n, err := f.Read(b)
+ if n != 0 || err != nil {
+ t.Errorf("Read(0) = %d, %v, want 0, nil", n, err)
+ }
+ b = make([]byte, 100)
+ n, err = f.Read(b)
+ if n <= 0 || err != nil {
+ t.Errorf("Read(100) = %d, %v, want >0, nil", n, err)
+ }
+}
+
func testReaddirnames(dir string, contents []string, t *testing.T) {
file, err := Open(dir)
defer file.Close()
コアとなるコードの解説
src/pkg/os/file.go
の変更
File.Read
メソッド内の条件式が変更されました。
if n == 0 && len(b) > 0 && !iserror(e) {
return 0, EOF
}
この行は、以下の3つの条件がすべて真である場合にのみEOF
を返すようにします。
n == 0
: 読み込んだバイト数が0である。len(b) > 0
: 読み込みバッファb
の長さが0より大きい。!iserror(e)
: システムコールからのエラーがない。
特に重要なのはlen(b) > 0
の追加です。これにより、呼び出し元がRead(make([]byte, 0))
のようにゼロ長のバッファを渡した場合、たとえn
が0でエラーがなくても、この条件が偽となるためEOF
は返されません。これは、io.Reader
の仕様に厳密に準拠した挙動であり、ゼロ長の読み込みはEOFを意味しないという原則を強化します。
src/pkg/os/os_test.go
の追加テスト
TestRead0
という新しいテスト関数が追加されました。
func TestRead0(t *testing.T) {
path := sfdir + "/" + sfname
f, err := Open(path)
if err != nil {
t.Fatal("open failed:", err)
}
defer f.Close()
b := make([]byte, 0)
n, err := f.Read(b)
if n != 0 || err != nil {
t.Errorf("Read(0) = %d, %v, want 0, nil", n, err)
}
b = make([]byte, 100)
n, err = f.Read(b)
if n <= 0 || err != nil {
t.Errorf("Read(100) = %d, %v, want >0, nil", n, err)
}
}
このテストは以下の2つの主要なアサーションを含んでいます。
-
ゼロ長読み込みの検証:
b := make([]byte, 0)
でゼロ長のバイトスライスを作成し、f.Read(b)
を呼び出します。 期待される結果はn == 0
かつerr == nil
です。もしEOF
が返されたり、他のエラーが発生したりすれば、テストは失敗します。これは、ゼロ長読み込みがEOFをトリガーしないことを保証します。 -
その後の通常の読み込みの検証: 次に、
b = make([]byte, 100)
で通常の長さのバイトスライスを作成し、再度f.Read(b)
を呼び出します。 期待される結果はn > 0
(少なくとも1バイトが読み込まれる)かつerr == nil
です。これは、ゼロ長読み込みがファイルの読み込み位置やその後の読み込み操作に悪影響を与えないことを確認します。
このテストの追加により、File.Read
メソッドの修正が意図した通りに機能し、io.Reader
のセマンティクスに準拠していることが保証されます。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/4853c51770f5e99d5d690801e5cb963848591587
- Go Issue 2402: https://golang.org/issue/2402
- Go Code Review: https://golang.org/cl/5298081
参考にした情報源リンク
- Go言語の
io.Reader
インターフェースに関する公式ドキュメントやブログ記事 - Go言語の
os
パッケージに関する公式ドキュメント - Go言語のIssueトラッカー(特にIssue 2402の議論)
- Go言語のソースコード(
src/pkg/os/file.go
およびsrc/pkg/os/os_test.go
) - 一般的なI/O操作におけるEOFの概念に関するプログラミングの知識
- Go言語のテストフレームワークに関する知識