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

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

このコミットは、Go言語の標準ライブラリ bytes パッケージ内の Buffer 型におけるバグ修正に関するものです。具体的には、ReadBytes メソッドの後に UnreadByte メソッドが正しく機能しない問題を解決しています。

コミット

commit 53e342f64805e0a2e750585e361d50d163616c9b
Author: Stéphane Travostino <stephane.travostino@gmail.com>
Date:   Fri Jan 11 17:02:21 2013 +1100

    bytes: fix UnreadByte failure after ReadBytes
    
    Fixes #4583.
    
    R=golang-dev, minux.ma, bradfitz, rsc, dave
    CC=golang-dev
    https://golang.org/cl/6976050

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

https://github.com/golang/go/commit/53e342f64805e0a2e750585e361d50d163616c9b

元コミット内容

bytes: fix UnreadByte failure after ReadBytes

このコミットは、bytes パッケージにおいて、ReadBytes メソッドの呼び出し後に UnreadByte メソッドが失敗する問題を修正するものです。

変更の背景

Go言語の bytes.Buffer は、可変長のバイトシーケンスを扱うためのバッファ実装です。io.Readerio.Writer インターフェースを実装しており、バイトデータの読み書きに広く利用されます。

Buffer には、読み取り操作を元に戻すための UnreadByte() メソッドが存在します。これは、直前に読み取った1バイトをバッファに戻し、次回の読み取り操作でそのバイトが再度読み取られるようにする機能です。しかし、ReadBytes() のような特定の読み取りメソッドの後に UnreadByte() を呼び出すと、期待通りに動作しないというバグが存在していました。

この問題は、ReadBytes が内部的に readSlice メソッドを呼び出す際に、バッファの内部状態を適切に更新していなかったことに起因します。UnreadByte は、直前の操作が読み取りであったかどうか、そしてその読み取りが何バイトであったかを記録する内部フラグ b.lastRead に依存しています。ReadBytes が完了した際にこのフラグが正しく設定されていなかったため、UnreadByte が「直前の操作が読み取りではない」と判断し、エラーを返していました。

コミットメッセージに Fixes #4583 とありますが、このイシューの詳細は公開されていません。しかし、コミット内容とテストケースから、ReadBytes の後に UnreadByte が機能しないという具体的な問題が修正されたことが明確に読み取れます。

前提知識の解説

bytes.Buffer

bytes.Buffer は、Go言語でバイト列を効率的に操作するための構造体です。メモリ上で動的にサイズが変更されるバッファを提供し、io.Reader および io.Writer インターフェースを実装しているため、ストリーム処理において非常に便利です。

ReadBytes(delim byte) ([]byte, error)

このメソッドは、バッファから delim で指定された区切り文字が出現するまで(またはEOFまで)バイトを読み取り、そのバイト列をスライスとして返します。区切り文字自体も返されるスライスに含まれます。読み取られたバイトはバッファから消費されます。

UnreadByte() error

このメソッドは、直前に読み取られた1バイトをバッファに戻します。これにより、次回の読み取り操作でそのバイトが再度読み取られるようになります。このメソッドは、直前の操作が読み取り操作でなかった場合や、バッファが空の場合などにエラーを返します。

b.lastRead (内部フィールド)

bytes.Buffer の内部には、直前の読み取り操作の種類を記録するための lastRead というフィールドが存在します。UnreadByte メソッドはこの lastRead の値を見て、直前の操作が Read 系のものであったか、そして何バイト読み取られたか(UnreadByte の場合は1バイト)を判断します。このフィールドが正しく設定されていないと、UnreadByte は期待通りに動作しません。

技術的詳細

問題の核心は、ReadBytes メソッドが内部的に呼び出す readSlice メソッドが、読み取り操作の完了時に bytes.Buffer の内部状態を適切に更新していなかった点にあります。

bytes.BufferUnreadByte メソッドは、直前の操作が読み取り操作であったことを確認するために、内部の lastRead フィールドを参照します。このフィールドは、ReadReadByte などの読み取りメソッドが呼び出された際に、その操作の種類を示す値(例えば opReadopReadByte)に設定されます。

しかし、ReadBytes が内部で利用する readSlice メソッドでは、読み取りが完了した後に b.lastReadopRead に設定する処理が欠落していました。このため、ReadBytes の直後に UnreadByte を呼び出すと、UnreadBytelastRead が適切な読み取り操作の値になっていないことを検出し、エラーを返してしまっていたのです。

修正は、readSlice メソッドの最後に b.lastRead = opRead という行を追加することで、この内部状態の不整合を解消しています。これにより、ReadBytes が完了した後に UnreadByte が呼び出されても、lastRead が正しく設定されているため、UnreadByte は正常に機能するようになります。

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

src/pkg/bytes/buffer.goreadSlice メソッドに以下の行が追加されました。

--- a/src/pkg/bytes/buffer.go
+++ b/src/pkg/bytes/buffer.go
@@ -377,6 +377,7 @@ func (b *Buffer) readSlice(delim byte) (line []byte, err error) {
 	}
 	line = b.buf[b.off:end]
 	b.off = end
+	b.lastRead = opRead
 	return line, err
 }

また、src/pkg/bytes/buffer_test.go には、この修正を検証するための新しいテストケース TestUnreadByte が追加されました。

--- a/src/pkg/bytes/buffer_test.go
+++ b/src/pkg/bytes/buffer_test.go
@@ -453,3 +453,25 @@ func TestReadEmptyAtEOF(t *testing.T) {
 		t.Errorf("wrong count; got %d want 0", n)
 	}
 }
+
+func TestUnreadByte(t *testing.T) {
+	b := new(Buffer)
+	b.WriteString("abcdefghijklmnopqrstuvwxyz")
+
+	_, err := b.ReadBytes('m')
+	if err != nil {
+		t.Fatalf("ReadBytes: %v", err)
+	}
+
+	err = b.UnreadByte()
+	if err != nil {
+		t.Fatalf("UnreadByte: %v", err)
+	}
+	c, err := b.ReadByte()
+	if err != nil {
+		t.Fatalf("ReadByte: %v", err)
+	}
+	if c != 'm' {
+		t.Errorf("ReadByte = %q; want %q", c, 'm')
+	}
+}

コアとなるコードの解説

readSlice メソッドの変更

readSlice メソッドは、ReadBytesReadString といった高レベルな読み取りメソッドから内部的に呼び出されるヘルパー関数です。この関数は、指定された区切り文字までバッファからバイトを読み取り、そのスライスを返します。

追加された b.lastRead = opRead の行は、readSlice が正常に読み取り操作を完了したことを Buffer の内部状態に明示的に記録します。opRead は、一般的な読み取り操作が行われたことを示す内部定数です。この設定により、UnreadByte メソッドが呼び出された際に、直前の操作が有効な読み取りであったと認識され、正しく1バイトをバッファに戻すことができるようになります。

TestUnreadByte テストケース

この新しいテストケースは、修正が正しく機能することを確認するために追加されました。

  1. new(Buffer) で新しい Buffer を作成し、"abcdefghijklmnopqrstuvwxyz" という文字列を書き込みます。
  2. b.ReadBytes('m') を呼び出し、文字 'm' が出現するまでバッファから読み取ります。この操作により、バッファの読み取り位置は 'm' の直後になります。
  3. b.UnreadByte() を呼び出し、直前に読み取られたバイト(この場合は 'm')をバッファに戻そうとします。修正前はここでエラーが発生していました。
  4. b.ReadByte() を呼び出し、バッファから1バイト読み取ります。UnreadByte が成功していれば、ここで読み取られるのは 'm' であるはずです。
  5. 最後に、読み取られたバイトが期待通り 'm' であることをアサートします。これにより、ReadBytes の後に UnreadByte が正しく機能し、バッファの読み取り位置が適切に巻き戻されたことが検証されます。

このテストケースは、バグの再現と修正の検証を簡潔かつ効果的に行っています。

関連リンク

参考にした情報源リンク