[インデックス 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:
bufiopackage: https://pkg.go.dev/bufio - Go Documentation:
iopackage: 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.Readerreturning (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が導入されたバージョン)