[インデックス 16202] ファイルの概要
このコミットは、Go言語の標準ライブラリ bufio
パッケージ内の Scanner
型の挙動を改善するものです。具体的には、io.Reader
インターフェースを実装するリーダーが Read
メソッドから (0, nil)
を繰り返し返した場合(つまり、データを読み込まないがエラーも返さない「不正な振る舞い」をする場合)に、Scanner
が無限ループに陥る可能性があった問題を修正します。この変更により、Scanner
はこのような状況でも最終的に io.ErrNoProgress
エラーを返して停止するようになります。
コミット
commit 591d4a47aefc74d96ec283abada57868a37d1f19
Author: Rob Pike <r@golang.org>
Date: Thu Apr 18 17:37:21 2013 -0700
bufio.Scan: don't stop after Read returns 0, nil
But stop eventually if the reader misbehaves.
R=golang-dev, bradfitz
CC=golang-dev
https://golang.org/cl/8757045
---
src/pkg/bufio/scan.go | 29 +++++++++++++++++++----------
src/pkg/bufio/scan_test.go | 18 +++++++++++++++++++
2 files changed, 37 insertions(+), 10 deletions(-)
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/591d4a47aefc74d96ec283abada57868a37d1f19
元コミット内容
bufio.Scan
は、Read
メソッドが (0, nil)
を返してもすぐに停止しないように変更されました。しかし、リーダーが不正な振る舞い(データを返さずに (0, nil)
を繰り返し返す)をした場合には、最終的に停止するように修正されました。
変更の背景
bufio.Scanner
は、入力ストリームからトークンを読み取るための便利なユーティリティです。内部的には、io.Reader
インターフェースを実装するオブジェクトからデータを読み取ります。io.Reader
の Read
メソッドは、読み取ったバイト数とエラーを返します。通常、データがない場合は (0, io.EOF)
を返すか、何らかのエラーを返します。
しかし、一部の io.Reader
の実装では、データが利用可能でないにもかかわらず、エラーも io.EOF
も返さずに (0, nil)
を返すことがあります。これは、io.Reader
の仕様に厳密には違反していませんが、Scanner
のようなコンシューマにとっては問題となります。なぜなら、Scanner
は (0, nil)
が返された場合、データがまだ利用可能になることを期待して、繰り返し Read
を呼び出し続ける可能性があるためです。
この「不正な振る舞い」をするリーダーが存在すると、Scanner
は無限ループに陥り、CPUリソースを消費し続け、プログラムがハングアップする原因となります。このコミットは、このような状況から Scanner
を保護し、予測可能な形で停止させることを目的としています。
前提知識の解説
bufio.Scanner
bufio.Scanner
は、Go言語の bufio
パッケージで提供される型で、入力ストリーム(通常は io.Reader
)からデータを読み取り、それをトークン(行、単語など)に分割するためのユーティリティです。Scan
メソッドを呼び出すことで次のトークンに進み、Bytes
や Text
メソッドでそのトークンを取得できます。
io.Reader
インターフェース
io.Reader
は、Go言語における基本的なインターフェースの一つで、データを読み取る能力を抽象化します。その定義は以下の通りです。
type Reader interface {
Read(p []byte) (n int, err error)
}
Read
メソッドは、p
に最大 len(p)
バイトのデータを読み込み、読み込んだバイト数 n
とエラー err
を返します。
n > 0
かつerr == nil
: 正常にn
バイト読み込んだ。n == 0
かつerr == io.EOF
: ストリームの終端に達した。n == 0
かつerr != nil
: 読み取り中にエラーが発生した。n > 0
かつerr != nil
:n
バイト読み込んだ後にエラーが発生した(この場合、n
バイトは有効なデータとして扱われるべきです)。
問題となるのは、n == 0
かつ err == nil
が繰り返し返されるケースです。これは、データが利用可能でないにもかかわらず、io.EOF
や他のエラーが返されない状況を指します。
io.EOF
io.EOF
は、io
パッケージで定義されている特別なエラー値で、入力ストリームの終端に達したことを示します。Read
メソッドが (0, io.EOF)
を返した場合、それ以上読み取るデータがないことを意味します。
io.ErrNoProgress
io.ErrNoProgress
は、Go 1.1 で導入された io
パッケージのエラーで、Read
メソッドが (0, nil)
を繰り返し返し、読み取りが進まない状況を示すために使用されます。このエラーは、無限ループを防ぐためのメカニズムとして導入されました。
技術的詳細
このコミットの主要な変更点は、bufio.Scanner
の Scan
メソッド内部のデータ読み取りロジックにあります。以前は、s.r.Read
から n == 0
が返された場合に s.setErr(io.EOF)
を呼び出して停止していました。これは、io.Reader
が (0, nil)
を返すことを想定していなかったため、無限ループの原因となっていました。
新しい実装では、s.r.Read
を呼び出す部分が for loop := 0; ; { ... }
という無限ループで囲まれています。このループの目的は、io.Reader
が (0, nil)
を繰り返し返すような不正な振る舞いをしても、Scanner
が無限に待機し続けないようにすることです。
ループ内で s.r.Read
を呼び出し、以下の条件でループを抜けます。
err != nil
: 読み取り中にエラーが発生した場合。このエラーはs.setErr(err)
でScanner
のエラーとして設定され、ループを抜けます。n > 0
: 少なくとも1バイトでもデータが読み取れた場合。これは読み取りが進んだことを意味するため、ループを抜けて次の処理に進みます。
そして、最も重要な変更が loop++
と if loop > 100
のチェックです。
loop++
:Read
が(0, nil)
を返すたびにloop
カウンタをインクリメントします。if loop > 100
:Read
が100回連続で(0, nil)
を返した場合、Scanner
はio.ErrNoProgress
エラーを設定し、ループを強制的に終了します。これにより、不正なリーダーによる無限ループを防ぎます。
この 100
というマジックナンバーは、ある程度の試行回数を許容しつつ、無限ループを検出するための閾値として設定されています。
テストファイル src/pkg/bufio/scan_test.go
には、endlessZeros
という新しいテスト用の io.Reader
実装が追加されています。この endlessZeros
は、常に (0, nil)
を返すように設計されており、このコミットで修正された問題(Scanner
が無限ループに陥る)を再現し、新しいロジックが io.ErrNoProgress
を返して正しく停止することを確認するために使用されます。
コアとなるコードの変更箇所
diff --git a/src/pkg/bufio/scan.go b/src/pkg/bufio/scan.go
index cebe92d331..2e1a2e9997 100644
--- a/src/pkg/bufio/scan.go
+++ b/src/pkg/bufio/scan.go
@@ -103,8 +103,7 @@ func (s *Scanner) Text() string {
// Scan advances the Scanner to the next token, which will then be
// available through the Bytes or Text method. It returns false when the
-// scan stops, either by reaching the end of the input, a zero-length
-// read from the input, or an error.
+// scan stops, either by reaching the end of the input or an error.
// After Scan returns false, the Err method will return any error that
// occurred during scanning, except that if it was io.EOF, Err
// will return nil.
@@ -159,15 +158,25 @@ func (s *Scanner) Scan() bool {
s.start = 0
continue
}
- // Finally we can read some input.
- n, err := s.r.Read(s.buf[s.end:len(s.buf)])
- if err != nil {
- s.setErr(err)
- }
- if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
- s.setErr(io.EOF)
+ // Finally we can read some input. Make sure we don't get stuck with
+ // a misbehaving Reader. Officially we don't need to do this, but let's
+ // be extra careful: Scanner is for safe, simple jobs.
+ for loop := 0; ; {
+ n, err := s.r.Read(s.buf[s.end:len(s.buf)])
+ s.end += n
+ if err != nil {
+ s.setErr(err)
+ break
+ }
+ if n > 0 {
+ break
+ }
+ loop++
+ if loop > 100 {
+ s.setErr(io.ErrNoProgress)
+ break
+ }
}
- s.end += n
}
}
diff --git a/src/pkg/bufio/scan_test.go b/src/pkg/bufio/scan_test.go
index 1b112f46da..c1483b2685 100644
--- a/src/pkg/bufio/scan_test.go
+++ b/src/pkg/bufio/scan_test.go
@@ -386,3 +386,21 @@ func TestNonEOFWithEmptyRead(t *testing.T) {
t.Errorf("unexpected error: %v", err)
}
}
+
+// Test that Scan finishes if we have endless empty reads.
+type endlessZeros struct{}
+
+func (endlessZeros) Read(p []byte) (int, error) {
+ return 0, nil
+}
+
+func TestBadReader(t *testing.T) {
+ scanner := NewScanner(endlessZeros{})
+ for scanner.Scan() {
+ t.Fatal("read should fail")
+ }
+ err := scanner.Err()
+ if err != io.ErrNoProgress {
+ t.Errorf("unexpected error: %v", err)
+ }
+}
コアとなるコードの解説
src/pkg/bufio/scan.go
の変更点
-
コメントの変更:
Scan
メソッドのドキュメントから「a zero-length read from the input」という記述が削除されました。これは、Read
が(0, nil)
を返してもすぐに停止しないという新しい挙動を反映しています。
-
読み取りロジックの変更:
- 以前は単一の
s.r.Read
呼び出しとそれに続くif n == 0
チェックがありました。 - 新しいコードでは、
for loop := 0; ; { ... }
という無限ループが導入されました。このループは、Scanner
がデータを読み取るまで、またはエラーが発生するまで、あるいは不正なリーダーが検出されるまでRead
を繰り返し呼び出すことを目的としています。 n, err := s.r.Read(s.buf[s.end:len(s.buf)])
: リーダーからデータを読み取ります。s.end += n
: 読み取ったバイト数n
をバッファの終端インデックスs.end
に加算します。if err != nil
: 読み取り中にエラーが発生した場合、そのエラーをScanner
のエラーとして設定し (s.setErr(err)
)、ループを抜けます (break
)。if n > 0
: 1バイトでもデータが読み取れた場合、読み取りが進んだと判断し、ループを抜けます (break
)。loop++
:n == 0
かつerr == nil
の場合(つまり、読み取りが進まなかった場合)、loop
カウンタをインクリメントします。if loop > 100
:loop
カウンタが100を超えた場合、これはリーダーが100回連続で(0, nil)
を返したことを意味します。この場合、s.setErr(io.ErrNoProgress)
を呼び出してio.ErrNoProgress
エラーを設定し、ループを強制的に終了します (break
)。これにより、無限ループを防ぎます。
- 以前は単一の
src/pkg/bufio/scan_test.go
の変更点
-
endlessZeros
型の追加:type endlessZeros struct{}
: 新しい型が定義されました。func (endlessZeros) Read(p []byte) (int, error) { return 0, nil }
: この型はio.Reader
インターフェースを実装し、常に(0, nil)
を返すようにします。これは、不正な振る舞いをするリーダーをシミュレートするためのものです。
-
TestBadReader
関数の追加:scanner := NewScanner(endlessZeros{})
:endlessZeros
のインスタンスをScanner
に渡して初期化します。for scanner.Scan() { t.Fatal("read should fail") }
:scanner.Scan()
をループで呼び出します。もしScan
がtrue
を返し続けた場合(つまり、無限ループに陥った場合)、t.Fatal
が呼び出されてテストが失敗します。期待されるのは、Scan
が最終的にfalse
を返すことです。err := scanner.Err()
:Scan
がfalse
を返した後、Scanner
のエラーを取得します。if err != io.ErrNoProgress { t.Errorf("unexpected error: %v", err) }
: 取得したエラーがio.ErrNoProgress
であることを確認します。これにより、Scanner
が不正なリーダーに対して正しくio.ErrNoProgress
を返して停止したことを検証します。
これらの変更により、bufio.Scanner
はより堅牢になり、不正な io.Reader
の実装に対しても安定して動作するようになりました。
関連リンク
- Go CL 8757045: https://golang.org/cl/8757045
参考にした情報源リンク
- Go Documentation:
bufio
package: https://pkg.go.dev/bufio - Go Documentation:
io
package: https://pkg.go.dev/io - Go Source Code:
io/io.go
(forErrNoProgress
): https://cs.opensource.google/go/go/+/refs/tags/go1.1:src/io/io.go;l=100 - Go Issue:
io.Reader
returning (0, nil) indefinitely: https://github.com/golang/go/issues/4436 (これは関連する議論の例であり、直接このコミットのトリガーではない可能性がありますが、文脈理解に役立ちます) - Go Blog: The Go 1.1 Release: https://go.dev/blog/go1.1 (
io.ErrNoProgress
が導入されたバージョン)