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

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

このコミットは、Go言語の標準ライブラリbytesパッケージ内のBuffer型におけるReadメソッドの挙動を修正するものです。具体的には、バッファが空の状態で長さ0のバイトスライスをReadメソッドに渡した場合に、io.EOFエラーを返すべきではないというバグを修正しています。これは、空のスライスをペイロードとしてRPC(Remote Procedure Call)を行う際に発生したコーナーケースとして発見されました。

コミット

commit 6a88f1c4cb212bc8c9ab7517b8eab2b4c20c6e67
Author: Rob Pike <r@golang.org>
Date:   Mon Dec 26 23:49:24 2011 -0800

    bytes.Buffer: read of 0 bytes at EOF shouldn't be an EOF
    This corner case arose doing an RPC with a empty-slice payload. Ouch.
    
    R=golang-dev, iant
    CC=golang-dev
    https://golang.org/cl/5505073
---
 src/pkg/bytes/buffer.go      |  5 ++++-
 src/pkg/bytes/buffer_test.go | 13 +++++++++++++
 2 files changed, 17 insertions(+), 1 deletion(-)

diff --git a/src/pkg/bytes/buffer.go b/src/pkg/bytes/buffer.go
index e66ac026e5..066023a3ec 100644
--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -200,13 +200,16 @@ func (b *Buffer) WriteRune(r rune) (n int, err error) {
 
 // Read reads the next len(p) bytes from the buffer or until the buffer
 // is drained.  The return value n is the number of bytes read.  If the
-// buffer has no data to return, err is io.EOF even if len(p) is zero;
+// buffer has no data to return, err is io.EOF (unless len(p) is zero);
 // otherwise it is nil.
 func (b *Buffer) Read(p []byte) (n int, err error) {
  	b.lastRead = opInvalid
  	if b.off >= len(b.buf) {
  		// Buffer is empty, reset to recover space.
  		b.Truncate(0)
+\t\tif len(p) == 0 {\n+\t\t\treturn\n+\t\t}\n  		return 0, io.EOF
  	}\n  	n = copy(p, b.buf[b.off:])
diff --git a/src/pkg/bytes/buffer_test.go b/src/pkg/bytes/buffer_test.go
index adb93302a5..d0af11f104 100644
--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -373,3 +373,16 @@ func TestReadBytes(t *testing.T) {\n \t\t}\n \t}\n }\n+\n+// Was a bug: used to give EOF reading empty slice at EOF.\n+func TestReadEmptyAtEOF(t *testing.T) {\n+\tb := new(Buffer)\n+\tslice := make([]byte, 0)\n+\tn, err := b.Read(slice)\n+\tif err != nil {\n+\t\tt.Errorf(\"read error: %v\", err)\n+\t}\n+\tif n != 0 {\n+\t\tt.Errorf(\"wrong count; got %d want 0\", n)\n+\t}\n+}\n```

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

[https://github.com/golang/go/commit/6a88f1c4cb212bc8c9ab7517b8eab2b4c20c6e67](https://github.com/golang/go/commit/6a88f1c4cb212bc8c9ab7517b8eab2b4c20c6e67)

## 元コミット内容

bytes.Buffer: read of 0 bytes at EOF shouldn't be an EOF
This corner case arose doing an RPC with a empty-slice payload. Ouch.

## 変更の背景

この変更は、`bytes.Buffer`の`Read`メソッドが、バッファが空の状態で長さ0のバイトスライス(`[]byte{}`)を引数として受け取った際に、誤って`io.EOF`を返してしまうというバグに対応するために行われました。コミットメッセージによると、この問題は「空のスライスをペイロードとしてRPCを行う際に発生したコーナーケース」として顕在化しました。

RPCのようなプロトコルでは、データが存在しないことを示すために空のバイトスライスを送信することがよくあります。受信側でこの空のペイロードを`bytes.Buffer`から読み取ろうとした際、`Read`メソッドが`io.EOF`を返してしまうと、それは「ストリームの終端に達し、これ以上データがない」という誤ったシグナルとして解釈されてしまいます。しかし、実際にはデータは「空」であって「終端」ではないため、これはプロトコルの解釈に混乱を招き、予期せぬエラーや挙動を引き起こす可能性がありました。

`io.Reader`インターフェースの一般的な慣習として、長さ0のバイトスライスに対する`Read`呼び出しは、エラーを返さずに`n=0`を返すことが期待されます。これは、読み取るべきデータがない場合でも、ストリームがまだ開いていることを示すためです。このコミットは、`bytes.Buffer`の`Read`メソッドをこの慣習に合わせることを目的としています。

## 前提知識の解説

このコミットを理解するためには、以下のGo言語の基本的な概念と`bytes.Buffer`の挙動に関する知識が必要です。

1.  **`io.Reader`インターフェース**:
    Go言語におけるデータの読み込み操作を抽象化する最も基本的なインターフェースです。
    ```go
    type Reader interface {
        Read(p []byte) (n int, err error)
    }
    ```
    `Read`メソッドは、`p`に最大`len(p)`バイトを読み込み、読み込んだバイト数`n`とエラー`err`を返します。
    `io.Reader`の重要な契約の一つは、以下の点です。
    *   **`n > 0`の場合**: `err`は`nil`であるか、または`io.EOF`以外のエラーであるべきです。`io.EOF`は、読み込みが成功したにもかかわらず、これ以上データがないことを示すために`n > 0`と共に返されることはありません。
    *   **`n == 0`の場合**:
        *   `err == nil`の場合: 読み込むべきデータが一時的にないが、後でデータが利用可能になる可能性があることを示します(例: ノンブロッキングI/O)。
        *   `err == io.EOF`の場合: ストリームの終端に達し、これ以上データがないことを示します。
        *   `err != nil`かつ`err != io.EOF`の場合: 読み込み中にエラーが発生したことを示します。
    特に、`Read(p []byte)`で`len(p)`が0の場合、つまり空のスライスを渡した場合、`io.Reader`の実装は通常、`n=0, err=nil`を返すことが期待されます。これは、読み込むべきデータがないため何も読み込まなかったが、ストリーム自体はまだ終端に達していないことを意味します。

2.  **`bytes.Buffer`**:
    `bytes.Buffer`は、可変長のバイトバッファを実装した型です。`io.Reader`、`io.Writer`、`io.ByteScanner`、`io.RuneScanner`インターフェースを実装しており、バイト列の読み書きを効率的に行うことができます。内部的にはバイトスライスを保持し、必要に応じてその容量を自動的に拡張します。
    `Buffer`の`Read`メソッドは、内部バッファからデータを読み取ります。バッファが空の場合、つまり読み取るべきデータがない場合、通常は`io.EOF`を返します。しかし、このコミットで修正される問題は、`len(p)`が0の場合の特殊なケースです。

3.  **`io.EOF`**:
    `io.EOF`は、入力が利用できなくなったことを示すエラーです。これは、ファイルやストリームの終端に達したことを示すために、`io.Reader`の`Read`メソッドによって返されます。

## 技術的詳細

このコミットが修正する問題は、`bytes.Buffer`の`Read`メソッドが、バッファが既に空であるにもかかわらず、引数として渡されたバイトスライス`p`の長さが0である場合に、`io.EOF`を返してしまうというものでした。

元のコードの`Read`メソッドは、バッファが空であるかどうかを`b.off >= len(b.buf)`という条件でチェックしていました。この条件が真の場合、つまりバッファが空の場合、コードは`return 0, io.EOF`を実行していました。

```go
func (b *Buffer) Read(p []byte) (n int, err error) {
 	b.lastRead = opInvalid
 	if b.off >= len(b.buf) {
 		// Buffer is empty, reset to recover space.
 		b.Truncate(0)
 		return 0, io.EOF // ここが問題
 	}
 	n = copy(p, b.buf[b.off:])
 	// ...
}

この挙動は、len(p)が0の場合でもio.EOFを返してしまうため、io.Readerインターフェースの一般的な期待(空のスライスでの読み込みはエラーなしでn=0を返す)に反していました。RPCのシナリオでは、空のペイロードを送信する際に、受信側がRead([]byte{})を呼び出すと、io.EOFを受け取ってしまい、データが「終端」したと誤解釈される可能性がありました。

修正は、return 0, io.EOFの前にlen(p) == 0のチェックを追加することによって行われました。

func (b *Buffer) Read(p []byte) (n int, err error) {
 	b.lastRead = opInvalid
 	if b.off >= len(b.buf) {
 		// Buffer is empty, reset to recover space.
 		b.Truncate(0)
+		if len(p) == 0 { // 追加された行
+			return // len(p)が0の場合は、n=0, err=nilを返す
+		}
 		return 0, io.EOF // len(p)が0でない場合は、n=0, err=io.EOFを返す
 	}
 	n = copy(p, b.buf[b.off:])
 	// ...
}

この変更により、バッファが空の状態でlen(p) == 0の場合、Readメソッドはn=0, err=nilを返します。これは、何も読み込まなかったが、ストリームはまだ終端に達していない(単に空のデータを読み取っただけ)という正しいセマンティクスを表現します。len(p)が0でない場合は、引き続きio.EOFを返すことで、バッファが空であることを適切に示します。

この修正は、bytes.Bufferio.Readerインターフェースの契約をより厳密に遵守し、特にゼロバイト読み込みのコーナーケースにおいて、より予測可能で正しい挙動をするようにするために重要でした。

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

src/pkg/bytes/buffer.goRead メソッド:

--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -200,13 +200,16 @@ func (b *Buffer) WriteRune(r rune) (n int, err error) {
 
 // Read reads the next len(p) bytes from the buffer or until the buffer
 // is drained.  The return value n is the number of bytes read.  If the
-// buffer has no data to return, err is io.EOF even if len(p) is zero;
+// buffer has no data to return, err is io.EOF (unless len(p) is zero);
 // otherwise it is nil.
 func (b *Buffer) Read(p []byte) (n int, err error) {
  	b.lastRead = opInvalid
  	if b.off >= len(b.buf) {
  		// Buffer is empty, reset to recover space.
  		b.Truncate(0)
+\t\tif len(p) == 0 {\n+\t\t\treturn\n+\t\t}\n  		return 0, io.EOF
  	}\n  	n = copy(p, b.buf[b.off:])

src/pkg/bytes/buffer_test.go に追加されたテストケース:

--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -373,3 +373,16 @@ func TestReadBytes(t *testing.T) {\n \t\t}\n \t}\n }\n+\n+// Was a bug: used to give EOF reading empty slice at EOF.\n+func TestReadEmptyAtEOF(t (t *testing.T) {\n+\tb := new(Buffer)\n+\tslice := make([]byte, 0)\n+\tn, err := b.Read(slice)\n+\tif err != nil {\n+\t\tt.Errorf(\"read error: %v\", err)\n+\t}\n+\tif n != 0 {\n+\t\tt.Errorf(\"wrong count; got %d want 0\", n)\n+\t}\n+}\n```

## コアとなるコードの解説

### `src/pkg/bytes/buffer.go` の変更

`Read`メソッドの冒頭部分、バッファが空であると判断されたブロック(`if b.off >= len(b.buf)`)内に、新しい条件分岐が追加されました。

```go
 		// Buffer is empty, reset to recover space.
 		b.Truncate(0)
+		if len(p) == 0 { // ここが追加された行
+			return // n=0, err=nil を返す
+		}
 		return 0, io.EOF // 以前からの行。len(p)が0でない場合に実行される
  • b.Truncate(0): バッファが空の場合、内部のバイトスライスをリセットしてメモリを解放します。これは既存の挙動です。
  • if len(p) == 0: ここが今回の修正の核心です。Readメソッドに渡されたスライスpの長さが0であるかをチェックします。
    • もしlen(p)が0であれば、returnステートメントが実行されます。Go言語では、戻り値が明示的に指定されていないreturnは、関数のシグネチャで定義されたゼロ値(この場合はn=0, err=nil)を返します。これにより、バッファが空であっても、空のスライスに対する読み込みではio.EOFを返さず、n=0, err=nilという期待される挙動になります。
    • もしlen(p)が0でなければ、つまり読み込むべきバイト数が指定されているにもかかわらずバッファが空である場合は、引き続きreturn 0, io.EOFが実行されます。これは、バッファの終端に達したことを正しく示します。

この変更により、bytes.BufferReadメソッドは、io.Readerインターフェースの一般的な契約、特にゼロバイト読み込みに関する慣習に適合するようになりました。

src/pkg/bytes/buffer_test.go の変更

TestReadEmptyAtEOFという新しいテスト関数が追加されました。

// Was a bug: used to give EOF reading empty slice at EOF.
func TestReadEmptyAtEOF(t *testing.T) {
	b := new(Buffer) // 新しいBufferを作成
	slice := make([]byte, 0) // 長さ0のバイトスライスを作成
	n, err := b.Read(slice) // 空のBufferから長さ0のスライスを読み込む
	if err != nil {
		t.Errorf("read error: %v", err) // エラーが返されたらテスト失敗
	}
	if n != 0 {
		t.Errorf("wrong count; got %d want 0", n) // 読み込んだバイト数が0でなければテスト失敗
	}
}
  • このテストは、まず新しい空のbytes.Bufferを作成します。
  • 次に、make([]byte, 0)を使って長さ0のバイトスライスsliceを作成します。
  • そして、b.Read(slice)を呼び出し、返されるnerrを検証します。
  • 期待される挙動は、errnilであり、n0であることです。もしerrnilでなかったり、n0でなかったりすれば、テストは失敗します。

このテストは、まさにこのコミットが修正しようとしているバグ(空のバッファから空のスライスを読み込んだときにio.EOFが返される問題)をピンポイントで検証するために書かれています。このテストがパスすることで、修正が正しく適用され、将来的に同様の回帰が発生しないことが保証されます。

関連リンク

参考にした情報源リンク