[インデックス 11945] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufioパッケージ内のReader.Peekメソッドの挙動を修正するものです。具体的には、Peekメソッドが有効なデータを返す場合でも、不適切にエラーを返してしまうバグ(Issue 3022)を修正することを目的としています。これにより、Peekが期待通りに動作し、呼び出し元が不要なエラー処理を行う必要がなくなります。
コミット
- コミットハッシュ:
88f8af127ab675b94e18f161c59415edd92110e9 - Author: Brad Fitzpatrick bradfitz@golang.org
- Date: Thu Feb 16 10:15:36 2012 +1100
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/88f8af127ab675b94e18f161c59415edd92110e9
元コミット内容
bufio: don't return errors from good Peeks
Fixes #3022
R=golang-dev, r
CC=golang-dev
https://golang.org/cl/5674060
変更の背景
bufioパッケージのReader.Peekメソッドは、内部バッファから指定されたバイト数(n)を読み込まずに「覗き見」する機能を提供します。このメソッドは、次に読み込まれるデータの一部を事前に確認したい場合に非常に便利です。しかし、このコミットが修正するバグ(Issue 3022)により、Peekメソッドが実際に要求されたバイト数分のデータを正常に提供できた場合でも、誤ってエラーを返してしまうという問題がありました。
具体的には、Peekは内部バッファのサイズや、基となるリーダーからの読み込み状況に応じて、io.EOFやErrBufferFullといったエラーを返すことがあります。しかし、このバグが存在するバージョンでは、Peekが要求されたnバイト分のデータを正常に提供できたにもかかわらず、以前の読み込み操作で発生したエラー(例えば、基となるリーダーが既にEOFに達していた場合など)を不適切に伝播させてしまうことがありました。
この挙動は、Peekの本来の意図に反しており、呼び出し元が不要なエラー処理を記述したり、Peekの戻り値の信頼性が低下したりする原因となっていました。このコミットは、Peekが「良い(good)」データを提供できた場合には、エラーを返さないようにすることで、この問題を解決し、Peekメソッドの堅牢性と使いやすさを向上させています。
前提知識の解説
bufioパッケージ
bufioパッケージは、I/O操作をバッファリングすることで効率化するためのGo言語の標準ライブラリです。ディスクI/OやネットワークI/Oなど、低速なI/O操作を直接行う代わりに、メモリ上のバッファを介してデータを読み書きすることで、システムコールの回数を減らし、パフォーマンスを向上させます。
bufio.Reader
bufio.Readerは、バッファリングされた読み込み操作を提供する型です。内部にバッファを持ち、基となるio.Readerからデータを読み込み、そのバッファを介して呼び出し元にデータを提供します。これにより、一度に多くのデータを読み込んでバッファに格納し、その後の小さな読み込み要求に対してはバッファから直接データを提供することで、効率的な読み込みを実現します。
Peekメソッド
func (b *Reader) Peek(n int) ([]byte, error)
Peekメソッドは、bufio.Readerの重要なメソッドの一つです。このメソッドは、次に読み込まれるであろうnバイトのデータを、実際に読み込みポインタを進めることなく(つまり、データを消費することなく)返します。これにより、呼び出し元はストリームの先頭を「覗き見」して、次にどのようなデータが来るかを判断し、それに基づいて適切な処理を行うことができます。
Peekは以下の2つの値を返します。
[]byte: 覗き見したデータ。要求されたnバイト分のデータがバッファに存在しない場合、利用可能な最大バイト数が返されます。error: エラーが発生した場合に返されます。主なエラーとしては、以下のものがあります。io.EOF: 基となるリーダーがファイルの終端に達した場合。bufio.ErrBufferFull: 要求されたnバイト分のデータをバッファに格納できない場合(バッファサイズが不足している場合)。
Go言語のエラーハンドリング
Go言語では、エラーは関数の最後の戻り値としてerror型の値で返されます。慣習として、エラーが発生しなかった場合はnilが返されます。呼び出し元は、返されたエラーがnilかどうかをチェックすることで、操作が成功したか失敗したかを判断します。
技術的詳細
このコミットの主要な変更は、src/pkg/bufio/bufio.go内のReader.Peekメソッドのロジック修正と、その修正を検証するためのテストケースの追加です。
bufio.goの変更点
変更前のPeekメソッドの関連部分は以下のようになっていました。
func (b *Reader) Peek(n int) ([]byte, error) {
// ... (前略) ...
if m > n {
m = n
}
err := b.readErr() // ここで以前のエラーを取得
if m < n && err == nil {
err = ErrBufferFull
}
return b.buf[b.r : b.r+m], err
}
このコードでは、まずb.readErr()を呼び出して、以前の読み込み操作で発生したエラー(例えば、基となるリーダーが既にEOFに達していた場合など)を取得していました。その後、m < n(要求されたnバイト分のデータがバッファに存在しない)かつerr == nil(以前のエラーがない)の場合にのみErrBufferFullを設定していました。
問題は、m < nの条件が満たされない場合、つまりm >= nの場合(要求されたnバイト分のデータがバッファに存在し、Peekが成功したと見なせる場合)でも、b.readErr()で取得した以前のエラーがそのまま返されてしまう点にありました。これが「良いPeek」でもエラーが返される原因でした。
修正後のPeekメソッドの関連部分は以下のようになります。
func (b *Reader) Peek(n int) ([]byte, error) {
// ... (前略) ...
if m > n {
m = n
}
var err error // エラー変数を初期化
if m < n { // 要求されたnバイト分のデータがバッファに存在しない場合のみエラーを考慮
err = b.readErr() // 以前のエラーを取得
if err == nil { // 以前のエラーがない場合、バッファ不足のエラーを設定
err = ErrBufferFull
}
}
return b.buf[b.r : b.r+m], err
}
この修正では、err変数をnilで初期化し、m < n(要求されたバイト数に満たない)の場合にのみエラーを考慮するロジックに変更されています。
m < nの場合:b.readErr()で以前のエラーを取得します。- もし以前のエラーが
nilであれば、ErrBufferFullを設定します。 - 以前のエラーが存在すれば、そのエラーが返されます(例:
io.EOF)。
m >= nの場合:errは初期値のnilのままとなり、Peekはエラーを返さずに成功します。
これにより、Peekが要求されたバイト数分のデータを正常に提供できた場合には、以前のエラーが伝播されることなく、nilが返されるようになります。
bufio_test.goの変更点
このコミットでは、修正されたPeekメソッドの挙動を検証するために、TestPeek関数に新しいテストケースが追加されています。
追加されたテストケースは、dataAndEOFReaderというカスタムのio.Reader実装を使用しています。このリーダーは、指定されたデータを一度だけ読み込み、その後すぐにio.EOFを返すように設計されています。
type dataAndEOFReader string
func (r dataAndEOFReader) Read(p []byte) (int, error) {
return copy(p, r), io.EOF
}
このdataAndEOFReaderを使ってbufio.Readerを初期化し、Peekメソッドがどのように振る舞うかをテストしています。
// Test for issue 3022, not exposing a reader's error on a successful Peek.
buf = NewReaderSize(dataAndEOFReader("abcd"), 32)
if s, err := buf.Peek(2); string(s) != "ab" || err != nil {
t.Errorf(`Peek(2) on "abcd", EOF = %q, %v; want "ab", nil`, string(s), err)
}
if s, err := buf.Peek(4); string(s) != "abcd" || err != nil {
t.Errorf(`Peek(4) on "abcd", EOF = %q, %v; want "abcd", nil`, string(s), err)
}
// ... (後略) ...
このテストでは、"abcd"というデータを持つdataAndEOFReaderを基にbufio.Readerを作成し、以下のことを確認しています。
Peek(2)を呼び出した場合、"ab"が返され、エラーはnilであること。Peek(4)を呼び出した場合、"abcd"が返され、エラーはnilであること。
これらのテストケースは、基となるリーダーがio.EOFを返している状況でも、Peekが要求されたバイト数分のデータを正常に提供できた場合には、エラーを返さないという修正後の挙動を正確に検証しています。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.goのReader.Peekメソッド内の以下の部分が変更されました。
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -106,9 +106,12 @@ func (b *Reader) Peek(n int) ([]byte, error) {
if m > n {
m = n
}
- err := b.readErr()
- if m < n && err == nil {
- err = ErrBufferFull
+ var err error
+ if m < n {
+ err = b.readErr()
+ if err == nil {
+ err = ErrBufferFull
+ }
}
return b.buf[b.r : b.r+m], err
}
コアとなるコードの解説
変更の核心は、エラーの伝播ロジックの変更にあります。
変更前は、err := b.readErr()によって、Peekが呼び出される前に基となるリーダーで発生した可能性のあるエラー(例えば、io.EOF)を無条件に取得していました。そして、m < n(要求されたバイト数に満たない)かつerr == nilの場合にのみErrBufferFullを設定していました。このロジックでは、m >= n(要求されたバイト数が全て提供できた)場合でも、b.readErr()で取得したエラーがそのまま返されてしまう問題がありました。
変更後は、var err errorでerrをnilで初期化し、エラーを考慮する条件をif m < nの中に限定しました。
if m < n(要求されたバイト数に満たない場合):err = b.readErr(): ここで初めて以前のエラーを取得します。if err == nil { err = ErrBufferFull }: 以前のエラーがなければ、バッファが不足していることを示すErrBufferFullを設定します。
m >= n(要求されたバイト数が全て提供できた場合):- この
if m < nブロックには入らないため、errは初期値のnilのままとなり、Peekはエラーを返さずに成功します。
- この
この変更により、Peekメソッドは、実際に要求されたバイト数分のデータを正常に提供できた場合には、基となるリーダーの以前のエラー状態に関わらず、nilを返すようになります。これにより、Peekのセマンティクスがより直感的になり、呼び出し元はPeekがデータを返した場合にはエラーがないことを信頼できるようになりました。
関連リンク
- Go Change-ID:
https://golang.org/cl/5674060(このコミットに対応するGoのコードレビューページ) - Go Issue 3022:
https://golang.org/issue/3022(このコミットが修正したバグのトラッキングページ)
参考にした情報源リンク
- コミットメッセージとコードの変更点
- Go言語の
bufioパッケージのドキュメント (Go標準ライブラリ) - Go言語のエラーハンドリングに関する一般的な知識
io.Readerインターフェースに関する知識io.EOFエラーに関する知識bufio.ErrBufferFullエラーに関する知識