[インデックス 19086] ファイルの概要
このコミットは、Go言語の標準ライブラリbufio
パッケージにおけるReader
型のUnreadByte
メソッドのバグを修正するものです。具体的には、UnreadByte
が複数回連続して呼び出された場合に、期待される動作(直前に読み込んだ1バイトのみをアンリードする)に反して、それ以前のバイトもアンリードできてしまう問題を解決しています。また、関連するテストのエラーメッセージの改善と対称性の向上も行われています。
コミット
commit 8bd9242f7cd27f69449b2cdae8acbf25a952134c
Author: Robert Griesemer <gri@golang.org>
Date: Wed Apr 9 14:19:13 2014 -0700
bufio: fix UnreadByte
Also:
- fix error messages in tests
- make tests more symmetric
Fixes #7607.
LGTM=r
R=r
CC=golang-codereviews
https://golang.org/cl/86180043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/8bd9242f7cd27f69449b2cdae8acbf25a952134c
元コミット内容
bufio: fix UnreadByte
Also:
- fix error messages in tests
- make tests more symmetric
Fixes #7607.
LGTM=r
R=r
CC=golang-codereviews
https://golang.org/cl/86180043
変更の背景
このコミットは、Goのbufio
パッケージのReader
型が提供するUnreadByte
メソッドの既存のバグ(Issue #7607)を修正するために行われました。UnreadByte
メソッドのドキュメントには、「直前に読み込んだバイトのみをアンリードできる」と明記されています。しかし、実際の動作では、このメソッドを連続して複数回呼び出すことが可能であり、その結果、直前に読み込んだバイトだけでなく、それ以前に読み込んだバイトまでバッファを巻き戻してしまうという問題がありました。
この予期せぬ動作は、bufio.Reader
の内部状態管理の不備に起因していました。特に、UnreadByte
が呼び出された際に、アンリードできるバイトが本当に直前に読み込まれたものだけであるかを厳密にチェックするメカニズムが不足していました。これにより、APIの契約と実際の挙動との間に乖離が生じ、bufio.Reader
を利用するアプリケーションで予期せぬバグやデータ破損が発生する可能性がありました。
このコミットは、この問題を解決し、UnreadByte
メソッドがそのドキュメントに記載された通りの振る舞いをするように修正することを目的としています。
前提知識の解説
bufio
パッケージ
bufio
パッケージは、Go言語におけるバッファリングされたI/O操作を提供します。これにより、ディスクI/OやネットワークI/Oなどの低レベルなI/O操作の回数を減らし、パフォーマンスを向上させることができます。bufio.Reader
は、io.Reader
インターフェースをラップし、内部バッファを使用してデータを効率的に読み込む機能を提供します。
bufio.Reader
の内部構造と動作
bufio.Reader
は、内部にバイトスライス(buf
)をバッファとして持ち、以下のフィールドでその状態を管理します。
buf []byte
: データを保持する内部バッファ。r int
: バッファ内の読み込み位置(次に読み込むバイトのインデックス)。w int
: バッファ内の書き込み位置(バッファにデータが読み込まれた最後のバイトの次のインデックス)。lastByte int
: 直前にReadByte
またはReadRune
で読み込まれたバイトを保持します。アンリード操作のために使用されます。初期値は-1。lastRuneSize int
: 直前にReadRune
で読み込まれたルーンのバイトサイズを保持します。アンリード操作のために使用されます。初期値は-1。
ReadByte()
メソッドが呼び出されると、b.buf[b.r]
から1バイトが読み込まれ、b.r
がインクリメントされます。読み込まれたバイトはb.lastByte
に保存されます。
UnreadByte()
メソッド
UnreadByte()
メソッドは、直前に読み込んだ1バイトをバッファに戻す(アンリードする)ために使用されます。これにより、次にReadByte()
が呼び出されたときに、同じバイトが再度読み込まれるようになります。このメソッドの重要な制約は、「直前に読み込んだバイトのみをアンリードできる」という点です。つまり、ReadByte()
が呼び出された直後に一度だけUnreadByte()
を呼び出すことが期待されます。複数回呼び出すとエラーになるか、予期せぬ動作を引き起こすべきではありません。
技術的詳細
修正前のUnreadByte
メソッドは、b.r
(読み込みポインタ)を単純にデクリメントすることでアンリードを実現していました。しかし、b.lastByte
フィールドが適切に管理されていなかったため、UnreadByte
が複数回呼び出された場合でも、b.r
がどんどん巻き戻されてしまい、直前に読み込んだバイト以外のデータもアンリードできてしまうという問題がありました。
このコミットでは、UnreadByte
のロジックが大幅に改善され、b.lastByte
とb.r
、b.w
の状態をより厳密にチェックするようになりました。
主な変更点は以下の通りです。
-
UnreadByte
の前提条件の強化: 修正前はif b.r <= 0
という条件でエラーを返していましたが、修正後はif b.lastByte < 0 || b.r == 0 && b.w > 0
というより厳密な条件でアンリードが不正であるかを判断します。b.lastByte < 0
: これは、直前にReadByte
またはReadRune
が呼び出されていない、または既にアンリードされていることを意味します。この場合、アンリードできるバイトが存在しないため、エラーを返します。b.r == 0 && b.w > 0
: これは、バッファの先頭にいるにもかかわらず、バッファにデータが残っている状態を示します。この状態でのアンリードは通常、不正な操作です。
-
b.r
の適切なデクリメント:b.r
が0より大きい場合は単純にb.r--
で読み込みポインタを戻します。 しかし、b.r == 0 && b.w == 0
(バッファが空の状態)の場合、これはReadByte
がバッファを使い切った後に呼び出されたことを意味します。この場合、UnreadByte
はバッファの先頭にバイトを書き戻す必要があります。そのため、b.w = 1
としてバッファの書き込み位置を1に設定し、b.buf[0]
にバイトを書き込みます。これにより、バッファが空の状態からでも正しくアンリードされたバイトを処理できるようになります。 -
b.lastByte
とb.lastRuneSize
のリセット: アンリードが成功した後、b.lastByte = -1
とb.lastRuneSize = -1
を設定することで、アンリードされたバイトが再度アンリードされるのを防ぎます。これにより、「直前に読み込んだバイトのみをアンリードできる」という制約が厳密に守られます。
これらの変更により、UnreadByte
は、直前に読み込んだバイトが存在し、かつそれがまだアンリードされていない場合にのみ成功するようになり、複数回の連続した呼び出しはErrInvalidUnreadByte
エラーを返すようになります。
コアとなるコードの変更箇所
このコミットによる主要なコード変更は、以下の2つのファイルに集中しています。
-
src/pkg/bufio/bufio.go
:ReadByte
メソッド内のループ条件がfor b.w == b.r
からfor b.r == b.w
に変更されました。これは機能的な変更ではなく、慣習的な記述への修正です。UnreadByte
メソッドのロジックが大幅に修正されました。特に、エラー条件のチェックと、b.r
およびb.w
の更新ロジックが変更されています。UnreadRune
メソッドのエラー条件がif b.lastRuneSize < 0 || b.r == 0
からif b.lastRuneSize < 0 || b.r < b.lastRuneSize
に変更されました。これはUnreadRune
のバグ修正も兼ねています。
-
src/pkg/bufio/bufio_test.go
:TestUnreadRune
とTestUnreadByte
内のエラーメッセージがより具体的で分かりやすいものに修正されました。- テストの対称性を向上させるために、
TestUnreadByteMultiple
という新しいテストケースが追加されました。このテストは、UnreadByte
が複数回呼び出された場合に正しくエラーを返すことを検証します。
コアとなるコードの解説
src/pkg/bufio/bufio.go
の変更点
UnreadByte
メソッド
// UnreadByte unreads the last byte. Only the most recently read byte can be unread.
func (b *Reader) UnreadByte() error {
- b.lastRuneSize = -1
- if b.r == b.w && b.lastByte >= 0 {
- b.w = 1
- b.r = 0
- b.buf[0] = byte(b.lastByte)
- b.lastByte = -1
- return nil
- }
- if b.r <= 0 {
+ if b.lastByte < 0 || b.r == 0 && b.w > 0 {
return ErrInvalidUnreadByte
}
- b.r--
+ // b.r > 0 || b.w == 0
+ if b.r > 0 {
+ b.r--
+ } else {
+ // b.r == 0 && b.w == 0
+ b.w = 1
+ }
+ b.buf[b.r] = byte(b.lastByte)
b.lastByte = -1
+ b.lastRuneSize = -1
return nil
}
- 旧ロジックの問題点: 以前のコードでは、
b.r == b.w && b.lastByte >= 0
という条件で特殊なケースを処理していましたが、これが複数回のアンリードを許してしまう原因となっていました。また、b.r <= 0
というエラーチェックも不十分でした。 - 新ロジックの改善点:
if b.lastByte < 0 || b.r == 0 && b.w > 0
:この新しい条件が、アンリードが不正な操作であるかを判断する主要なゲートとなります。b.lastByte < 0
:ReadByte
やReadRune
が直前に呼び出されていない、または既にアンリードされている場合、アンリードできるバイトがないためエラーを返します。b.r == 0 && b.w > 0
: バッファの先頭にいるにもかかわらず、バッファにまだ読み込まれていないデータがある場合、これは通常、不正な状態でありエラーを返します。
if b.r > 0
: 通常のケースで、読み込みポインタb.r
をデクリメントしてバイトをアンリードします。else { b.w = 1 }
:b.r == 0 && b.w == 0
(バッファが完全に空の状態)の場合、これはReadByte
がバッファから最後のバイトを読み取った直後であることを意味します。この場合、アンリードされたバイトをバッファの先頭(インデックス0)に書き戻すために、b.w
を1に設定します。b.buf[b.r] = byte(b.lastByte)
: アンリードされたバイトをバッファの正しい位置に書き戻します。b.lastByte = -1
とb.lastRuneSize = -1
: アンリードが成功した後、これらの値をリセットすることで、同じバイトが再度アンリードされるのを防ぎ、UnreadByte
の「直前に読み込んだバイトのみ」という制約を厳密に強制します。
UnreadRune
メソッド
// UnreadRune unreads the last rune. If the most recent read operation was not
// a ReadRune, it returns an error. (In this regard it is stricter than UnreadByte,
// which will unread the last byte from any read operation.)
func (b *Reader) UnreadRune() error {
- if b.lastRuneSize < 0 || b.r == 0 {
+ if b.lastRuneSize < 0 || b.r < b.lastRuneSize {
return ErrInvalidUnreadRune
}
b.r -= b.lastRuneSize
UnreadRune
のエラー条件も改善されました。b.r < b.lastRuneSize
という条件が追加され、アンリードしようとしているルーンのサイズ分、読み込みポインタを戻せるかどうかがより正確にチェックされるようになりました。
src/pkg/bufio/bufio_test.go
の変更点
- 既存のテストケース(
TestUnreadRune
,TestUnreadByte
)のエラーメッセージが、t.Error
からt.Fatal
やより詳細なメッセージに変更され、テスト失敗時のデバッグが容易になりました。 TestUnreadByteMultiple
の追加: この新しいテストは、UnreadByte
が複数回呼び出された場合の挙動を検証します。具体的には、ReadByte
でNバイト読み込んだ後、UnreadByte
を一度呼び出し、その後再度UnreadByte
を呼び出してエラーが返されることを確認します。これにより、UnreadByte
が一度しか機能しないという新しい制約が正しく実装されていることが保証されます。
これらの変更により、bufio.Reader
のUnreadByte
およびUnreadRune
メソッドは、より堅牢で、ドキュメントに記載された通りの正確な動作をするようになりました。
関連リンク
- Go Issue #7607: https://github.com/golang/go/issues/7607
- Go CL 86180043: https://golang.org/cl/86180043
参考にした情報源リンク
- Web search results for "Go bufio UnreadByte fix #7607 8bd9242f7cd27f69449b2cdae8acbf25a952134c" (Accessed via Google Search API)
- https://github.com/golang/go/commit/8bd9242f7cd27f69449b2cdae8acbf25a952134c