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

[インデックス 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.ReaderRead メソッドは、読み取ったバイト数とエラーを返します。通常、データがない場合は (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 メソッドを呼び出すことで次のトークンに進み、BytesText メソッドでそのトークンを取得できます。

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.ScannerScan メソッド内部のデータ読み取りロジックにあります。以前は、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 を呼び出し、以下の条件でループを抜けます。

  1. err != nil: 読み取り中にエラーが発生した場合。このエラーは s.setErr(err)Scanner のエラーとして設定され、ループを抜けます。
  2. n > 0: 少なくとも1バイトでもデータが読み取れた場合。これは読み取りが進んだことを意味するため、ループを抜けて次の処理に進みます。

そして、最も重要な変更が loop++if loop > 100 のチェックです。

  • loop++: Read(0, nil) を返すたびに loop カウンタをインクリメントします。
  • if loop > 100: Read が100回連続で (0, nil) を返した場合、Scannerio.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() をループで呼び出します。もし Scantrue を返し続けた場合(つまり、無限ループに陥った場合)、t.Fatal が呼び出されてテストが失敗します。期待されるのは、Scan が最終的に false を返すことです。
    • err := scanner.Err(): Scanfalse を返した後、Scanner のエラーを取得します。
    • if err != io.ErrNoProgress { t.Errorf("unexpected error: %v", err) }: 取得したエラーが io.ErrNoProgress であることを確認します。これにより、Scanner が不正なリーダーに対して正しく io.ErrNoProgress を返して停止したことを検証します。

これらの変更により、bufio.Scanner はより堅牢になり、不正な io.Reader の実装に対しても安定して動作するようになりました。

関連リンク

参考にした情報源リンク