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

[インデックス 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メソッドの戻り値nerrには以下の重要なセマンティクスがあります。

  • n > 0: 少なくとも1バイトが読み込まれたことを示します。errnilであるか、読み込み中に発生した非致命的なエラー(例: 一部のバイトが読み込まれた後にネットワーク接続が切断された場合など)を示すことがあります。
  • n == 0かつerr == nil: これは、Readがブロックせずに0バイトを読み込んだことを意味します。これは、データが一時的に利用できない場合(非ブロッキングI/O)や、pがゼロ長のバイトスライスである場合に発生します。これはEOFを意味しません。
  • n == 0かつerr == io.EOF: これは、ファイルの終端に達し、それ以上読み込むデータがないことを明確に示します。
  • n == 0かつerr != nilio.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.goTestRead0という新しいテストケースが追加されました。このテストは、ゼロ長のバイトスライスを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を返すようにします。

  1. n == 0: 読み込んだバイト数が0である。
  2. len(b) > 0: 読み込みバッファbの長さが0より大きい。
  3. !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つの主要なアサーションを含んでいます。

  1. ゼロ長読み込みの検証: b := make([]byte, 0)でゼロ長のバイトスライスを作成し、f.Read(b)を呼び出します。 期待される結果はn == 0かつerr == nilです。もしEOFが返されたり、他のエラーが発生したりすれば、テストは失敗します。これは、ゼロ長読み込みがEOFをトリガーしないことを保証します。

  2. その後の通常の読み込みの検証: 次に、b = make([]byte, 100)で通常の長さのバイトスライスを作成し、再度f.Read(b)を呼び出します。 期待される結果はn > 0(少なくとも1バイトが読み込まれる)かつerr == nilです。これは、ゼロ長読み込みがファイルの読み込み位置やその後の読み込み操作に悪影響を与えないことを確認します。

このテストの追加により、File.Readメソッドの修正が意図した通りに機能し、io.Readerのセマンティクスに準拠していることが保証されます。

関連リンク

参考にした情報源リンク

  • 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言語のテストフレームワークに関する知識