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

[インデックス 16159] ファイルの概要

このコミットは、Go言語の標準ライブラリbufioパッケージ内のScanner型におけるEOF(End Of File)時のエラーハンドリングの不具合を修正するものです。具体的には、Scanner.Scan()メソッドが入力の終端に達した際、またはリーダーが0バイトを返した場合の挙動を改善し、予期せぬエラー(io.ErrUnexpectedEOFなど)が正しく伝播されるようにしています。

コミット

commit 082a4a8a47d03c5fd246b4d079391bdb21f2c3ed
Author: Rob Pike <r@golang.org>
Date:   Wed Apr 10 20:58:19 2013 -0700

    bufio/Scan: fix error handling at EOF
    Fixes #5268.
    
    R=golang-dev, dsymonds, bradfitz
    CC=golang-dev
    https://golang.org/cl/8646045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/082a4a8a47d03c5fd246b4d079391bdb21f2c3ed

元コミット内容

このコミットは、bufio/ScanパッケージにおけるEOF時のエラーハンドリングを修正します。具体的には、Scanner.Scan()メソッドが入力の終端に達した際、またはリーダーが0バイトを返した場合に、io.EOF以外のエラーがScanner.Err()によって正しく返されるように変更されています。これにより、Issue 5268で報告された問題が解決されます。

変更の背景

Go言語のbufioパッケージは、I/O操作をバッファリングすることで効率を高めるための機能を提供します。Scanner型は、入力ストリームをトークン(単語、行など)に分割するための便利なインターフェースを提供します。

このコミットが行われる前、Scanner.Scan()メソッドには、入力リーダーが0バイトを返した場合の特定のエラーハンドリングに問題がありました。通常、リーダーが0バイトを返し、かつエラーも返さない場合、それはEOF(End Of File)を示します。しかし、リーダーが0バイトを返すと同時にio.EOF以外のエラー(例えば、io.ErrUnexpectedEOFなど)を返した場合、Scannerは内部的にそのエラーをio.EOFで上書きしてしまう可能性がありました。

これにより、Scanner.Err()メソッドを呼び出した際に、実際に発生したエラーではなく、常にio.EOF(またはnil)が返されてしまい、呼び出し元が真のエラー原因を特定できないという問題が発生していました。Issue 5268は、この挙動が予期せぬエラーを隠蔽し、デバッグを困難にすることを指摘していました。

この修正の目的は、Scannerがリーダーから受け取った実際のエラーを正確に保持し、Scan()falseを返した後にErr()メソッドを通じてそのエラーを呼び出し元に適切に報告することです。

前提知識の解説

  • bufioパッケージ: Go言語の標準ライブラリの一部で、バッファリングされたI/Oを提供します。これにより、ディスクやネットワークからの読み書きの効率が向上します。
  • bufio.Scanner: bufioパッケージ内で提供される型で、io.Readerからデータを読み込み、それをトークン(行、単語、カスタム区切り文字など)に分割するための高レベルなインターフェースを提供します。
    • Scan()メソッド: Scannerを次のトークンに進めます。トークンが見つかればtrueを返し、入力の終端に達したかエラーが発生した場合はfalseを返します。
    • Bytes()/Text()メソッド: Scan()trueを返した後に、現在のトークンをバイトスライスまたは文字列として返します。
    • Err()メソッド: Scan()falseを返した後に、スキャン中に発生したエラーを返します。io.EOFの場合はnilを返します。
  • io.Readerインターフェース: Go言語における基本的な読み込みインターフェースです。Read(p []byte) (n int, err error)メソッドを持ち、データをpに読み込み、読み込んだバイト数nとエラーerrを返します。
  • io.EOF: io.ReaderReadメソッドが、それ以上読み込むデータがないことを示すために返す特別なエラー値です。通常、Readn > 0, io.EOFまたはn == 0, io.EOFを返します。
  • io.ErrUnexpectedEOF: ioパッケージで定義されているエラーの一つで、予期せぬEOFが発生した場合に返されることがあります。例えば、期待されるバイト数に満たない状態でストリームが終了した場合などです。
  • エラーハンドリング: Go言語では、関数がエラーを返す場合、通常は戻り値の最後の要素としてerror型の値を返します。呼び出し元は、このエラー値をチェックして適切な処理を行います。

技術的詳細

このコミットの核心は、bufio.ScannerScan()メソッド内のエラー処理ロジックの変更にあります。

Scanner.Scan()メソッドは、内部的にs.readAhead()を呼び出して、次のトークンを読み込むためのデータをバッファに確保しようとします。s.readAhead()は、基となるio.Readerからデータを読み込みます。

変更前のコードでは、s.readAhead()n == 0(読み込んだバイト数が0)かつerr != nilの場合、特にerrio.EOFでない場合に問題がありました。元のコードは以下のようになっていました。

// src/pkg/bufio/scan.go (変更前)
if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
    s.err = io.EOF // ここでエラーがio.EOFに上書きされる可能性があった
}

このロジックは、「リーダーがEOFを返さない場合に無限ループに陥るのを防ぐ」という意図がありましたが、副作用として、リーダーが0バイトを返しながらio.EOF以外の実際のエラー(例: io.ErrUnexpectedEOF)を返した場合でも、Scannerの内部エラー状態s.errが強制的にio.EOFに設定されてしまうという問題がありました。これにより、Scanner.Err()を呼び出しても、真のエラーが隠蔽されてしまうことになります。

修正後のコードでは、このs.err = io.EOFの直接代入がs.setErr(io.EOF)に置き換えられました。

// src/pkg/bufio/scan.go (変更後)
if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
    s.setErr(io.EOF) // setErrメソッドを通じてエラーが設定される
}

s.setErr()メソッドは、Scannerの内部エラー状態を設定するためのヘルパーメソッドです。このメソッドの内部ロジックは、既にエラーが設定されている場合は新しいエラーで上書きしない、という賢い挙動をします。

// setErr records the first error encountered.
func (s *Scanner) setErr(err error) {
	if s.err == nil || s.err == io.EOF { // 既存のエラーがないか、io.EOFの場合のみ設定
		s.err = err
	}
}

この変更により、s.readAhead()io.EOF以外のエラーを返した場合(例えば、io.ErrUnexpectedEOF)、そのエラーはs.setErr(err)によって一度s.errに設定されます。その後、n == 0の条件が満たされたとしても、s.setErr(io.EOF)が呼び出された際に、既にs.errio.EOF以外のエラーが設定されているため、そのエラーがio.EOFで上書きされることはなくなります。結果として、Scanner.Err()は実際に発生したエラーを正確に報告できるようになります。

また、Scanner.Scan()メソッドのドキュメントも更新され、Scanfalseを返す条件に「a zero-length read from the input」(入力からのゼロ長読み込み)が追加され、より正確な説明が提供されています。

テストケースTestNonEOFWithEmptyReadが追加され、この修正が正しく機能することを確認しています。このテストでは、常に0, io.ErrUnexpectedEOFを返すカスタムio.Readerを実装し、Scannerio.ErrUnexpectedEOFを正しく報告するかどうかを検証しています。

コアとなるコードの変更箇所

src/pkg/bufio/scan.go

--- a/src/pkg/bufio/scan.go
+++ b/src/pkg/bufio/scan.go
@@ -103,7 +103,8 @@ 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 or an error.
+// scan stops, either by reaching the end of the input, a zero-length
+// read from 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.
@@ -164,7 +165,7 @@ func (s *Scanner) Scan() bool {
 			s.setErr(err)
 		}
 		if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
-			s.err = io.EOF
+			s.setErr(io.EOF)
 		}
 		s.end += n
 	}

src/pkg/bufio/scan_test.go

--- a/src/pkg/bufio/scan_test.go
+++ b/src/pkg/bufio/scan_test.go
@@ -368,3 +368,21 @@ func TestErrAtEOF(t *testing.T) {
 		t.Fatal("wrong error:", s.Err())
 	}
 }
+
+// Test for issue 5268.
+type alwaysError struct{}
+
+func (alwaysError) Read(p []byte) (int, error) {
+	return 0, io.ErrUnexpectedEOF
+}
+
+func TestNonEOFWithEmptyRead(t *testing.T) {
+	scanner := NewScanner(alwaysError{})
+	for scanner.Scan() {
+		t.Fatal("read should fail")
+	}
+	err := scanner.Err()
+	if err != io.ErrUnexpectedEOF {
+		t.Errorf("unexpected error: %v", err)
+	}
+}

コアとなるコードの解説

src/pkg/bufio/scan.go の変更

  1. Scan()メソッドのドキュメント更新: Scan()メソッドのコメントが更新され、falseを返す条件に「a zero-length read from the input」(入力からのゼロ長読み込み)が追加されました。これは、リーダーが0バイトを返した場合もスキャンが停止する可能性があることを明確にしています。

  2. エラー設定ロジックの変更: Scan()メソッド内のif n == 0 { ... }ブロックにおいて、s.err = io.EOFという直接代入がs.setErr(io.EOF)に変更されました。 この変更が重要です。s.setErr()メソッドは、既にエラーが設定されている場合(特にio.EOF以外のエラーが先に発生している場合)には、そのエラーを上書きしないロジックを持っています。これにより、io.Reader0, io.ErrUnexpectedEOFのような値を返した場合、まずs.setErr(err)によってio.ErrUnexpectedEOFs.errに設定され、その後のs.setErr(io.EOF)の呼び出しでは、既にエラーが設定されているためio.EOFで上書きされることがなくなります。結果として、Scanner.Err()io.ErrUnexpectedEOFを正しく返すようになります。

src/pkg/bufio/scan_test.go の変更

  1. alwaysError構造体の追加: io.Readerインターフェースを満たすalwaysErrorという新しい型が定義されました。この型は、Readメソッドが常に0, io.ErrUnexpectedEOFを返すように実装されています。これは、特定のシナリオ(リーダーが0バイトを返しつつ、io.EOF以外のエラーを返す場合)をシミュレートするために使用されます。

  2. TestNonEOFWithEmptyReadテスト関数の追加: この新しいテスト関数は、Issue 5268で報告された問題を再現し、修正が正しく機能することを確認します。

    • NewScanner(alwaysError{})で、常にio.ErrUnexpectedEOFを返すリーダーを持つScannerを作成します。
    • for scanner.Scan() { ... }ループは、Scan()falseを返すまで実行されます。alwaysErrorリーダーは常に0, io.ErrUnexpectedEOFを返すため、Scan()はすぐにfalseを返してループは実行されません。
    • err := scanner.Err()で、Scannerが保持しているエラーを取得します。
    • if err != io.ErrUnexpectedEOF { ... }で、取得したエラーが期待されるio.ErrUnexpectedEOFであるかどうかを検証します。もし修正が適用されていなければ、このテストは失敗し、errnilまたはio.EOFになってしまうでしょう。

これらの変更により、bufio.Scannerはより堅牢なエラーハンドリングを提供し、基となるリーダーから返されるエラーを正確に伝播できるようになりました。

関連リンク

  • Go言語のbufioパッケージドキュメント: https://pkg.go.dev/bufio
  • Go言語のioパッケージドキュメント: https://pkg.go.dev/io
  • Go言語のIssue 5268: bufio: Scanner.Err() returns nil for non-EOF errors (このコミットが修正した問題の報告) - GitHubのコミットページからリンクを辿るか、GoのIssueトラッカーで検索することで詳細を確認できます。

参考にした情報源リンク

  • GitHub: golang/go commit 082a4a8a47d03c5fd246b4d079391bdb21f2c3ed
  • Go言語の公式ドキュメント (pkg.go.dev)
  • Go言語のソースコード (特にsrc/pkg/bufio/scan.gosrc/pkg/bufio/scan_test.go)
  • Go言語のIssueトラッカー (Issue 5268に関する情報)

[インデックス 16159] ファイルの概要

このコミットは、Go言語の標準ライブラリbufioパッケージ内のScanner型におけるEOF(End Of File)時のエラーハンドリングの不具合を修正するものです。具体的には、Scanner.Scan()メソッドが入力の終端に達した際、またはリーダーが0バイトを返した場合の挙動を改善し、予期せぬエラー(io.ErrUnexpectedEOFなど)が正しく伝播されるようにしています。

コミット

commit 082a4a8a47d03c5fd246b4d079391bdb21f2c3ed
Author: Rob Pike <r@golang.org>
Date:   Wed Apr 10 20:58:19 2013 -0700

    bufio/Scan: fix error handling at EOF
    Fixes #5268.
    
    R=golang-dev, dsymonds, bradfitz
    CC=golang-dev
    https://golang.org/cl/8646045

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/082a4a8a47d03c5fd246b4d079391bdb21f2c3ed

元コミット内容

このコミットは、bufio/ScanパッケージにおけるEOF時のエラーハンドリングを修正します。具体的には、Scanner.Scan()メソッドが入力の終端に達した際、またはリーダーが0バイトを返した場合に、io.EOF以外のエラーがScanner.Err()によって正しく返されるように変更されています。これにより、Issue 5268で報告された問題が解決されます。

変更の背景

Go言語のbufioパッケージは、I/O操作をバッファリングすることで効率を高めるための機能を提供します。Scanner型は、入力ストリームをトークン(単語、行など)に分割するための便利なインターフェースを提供します。

このコミットが行われる前、Scanner.Scan()メソッドには、入力リーダーが0バイトを返した場合の特定のエラーハンドリングに問題がありました。通常、リーダーが0バイトを返し、かつエラーも返さない場合、それはEOF(End Of File)を示します。しかし、リーダーが0バイトを返すと同時にio.EOF以外のエラー(例えば、io.ErrUnexpectedEOFなど)を返した場合、Scannerは内部的にそのエラーをio.EOFで上書きしてしまう可能性がありました。

これにより、Scanner.Err()メソッドを呼び出した際に、実際に発生したエラーではなく、常にio.EOF(またはnil)が返されてしまい、呼び出し元が真のエラー原因を特定できないという問題が発生していました。Issue 5268は、この挙動が予期せぬエラーを隠蔽し、デバッグを困難にすることを指摘していました。

この修正の目的は、Scannerがリーダーから受け取った実際のエラーを正確に保持し、Scan()falseを返した後にErr()メソッドを通じてそのエラーを呼び出し元に適切に報告することです。

前提知識の解説

  • bufioパッケージ: Go言語の標準ライブラリの一部で、バッファリングされたI/Oを提供します。これにより、ディスクやネットワークからの読み書きの効率が向上します。
  • bufio.Scanner: bufioパッケージ内で提供される型で、io.Readerからデータを読み込み、それをトークン(行、単語、カスタム区切り文字など)に分割するための高レベルなインターフェースを提供します。
    • Scan()メソッド: Scannerを次のトークンに進めます。トークンが見つかればtrueを返し、入力の終端に達したかエラーが発生した場合はfalseを返します。
    • Bytes()/Text()メソッド: Scan()trueを返した後に、現在のトークンをバイトスライスまたは文字列として返します。
    • Err()メソッド: Scan()falseを返した後に、スキャン中に発生したエラーを返します。io.EOFの場合はnilを返します。
  • io.Readerインターフェース: Go言語における基本的な読み込みインターフェースです。Read(p []byte) (n int, err error)メソッドを持ち、データをpに読み込み、読み込んだバイト数nとエラーerrを返します。
  • io.EOF: io.ReaderReadメソッドが、それ以上読み込むデータがないことを示すために返す特別なエラー値です。通常、Readn > 0, io.EOFまたはn == 0, io.EOFを返します。
  • io.ErrUnexpectedEOF: ioパッケージで定義されているエラーの一つで、予期せぬEOFが発生した場合に返されることがあります。例えば、期待されるバイト数に満たない状態でストリームが終了した場合などです。
  • エラーハンドリング: Go言語では、関数がエラーを返す場合、通常は戻り値の最後の要素としてerror型の値を返します。呼び出し元は、このエラー値をチェックして適切な処理を行います。

技術的詳細

このコミットの核心は、bufio.ScannerScan()メソッド内のエラー処理ロジックの変更にあります。

Scanner.Scan()メソッドは、内部的にs.readAhead()を呼び出して、次のトークンを読み込むためのデータをバッファに確保しようとします。s.readAhead()は、基となるio.Readerからデータを読み込みます。

変更前のコードでは、s.readAhead()n == 0(読み込んだバイト数が0)かつerr != nilの場合、特にerrio.EOFでない場合に問題がありました。元のコードは以下のようになっていました。

// src/pkg/bufio/scan.go (変更前)
if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
    s.err = io.EOF // ここでエラーがio.EOFに上書きされる可能性があった
}

このロジックは、「リーダーがEOFを返さない場合に無限ループに陥るのを防ぐ」という意図がありましたが、副作用として、リーダーが0バイトを返しながらio.EOF以外の実際のエラー(例: io.ErrUnexpectedEOF)を返した場合でも、Scannerの内部エラー状態s.errが強制的にio.EOFに設定されてしまうという問題がありました。これにより、Scanner.Err()を呼び出しても、真のエラーが隠蔽されてしまうことになります。

修正後のコードでは、このs.err = io.EOFの直接代入がs.setErr(io.EOF)に置き換えられました。

// src/pkg/bufio/scan.go (変更後)
if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
    s.setErr(io.EOF) // setErrメソッドを通じてエラーが設定される
}

s.setErr()メソッドは、Scannerの内部エラー状態を設定するためのヘルパーメソッドです。このメソッドの内部ロジックは、既にエラーが設定されている場合は新しいエラーで上書きしない、という賢い挙動をします。

// setErr records the first error encountered.
func (s *Scanner) setErr(err error) {
	if s.err == nil || s.err == io.EOF { // 既存のエラーがないか、io.EOFの場合のみ設定
		s.err = err
	}
}

この変更により、s.readAhead()io.EOF以外のエラーを返した場合(例えば、io.ErrUnexpectedEOF)、そのエラーはs.setErr(err)によって一度s.errに設定されます。その後、n == 0の条件が満たされたとしても、s.setErr(io.EOF)が呼び出された際に、既にs.errio.EOF以外のエラーが設定されているため、そのエラーがio.EOFで上書きされることはなくなります。結果として、Scanner.Err()は実際に発生したエラーを正確に報告できるようになります。

また、Scanner.Scan()メソッドのドキュメントも更新され、Scanfalseを返す条件に「a zero-length read from the input」(入力からのゼロ長読み込み)が追加され、より正確な説明が提供されています。

テストケースTestNonEOFWithEmptyReadが追加され、この修正が正しく機能することを確認しています。このテストでは、常に0, io.ErrUnexpectedEOFを返すカスタムio.Readerを実装し、Scannerio.ErrUnexpectedEOFを正しく報告するかどうかを検証しています。

コアとなるコードの変更箇所

src/pkg/bufio/scan.go

--- a/src/pkg/bufio/scan.go
+++ b/src/pkg/bufio/scan.go
@@ -103,7 +103,8 @@ 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 or an error.
+// scan stops, either by reaching the end of the input, a zero-length
+// read from 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.
@@ -164,7 +165,7 @@ func (s *Scanner) Scan() bool {
 			s.setErr(err)
 		}
 		if n == 0 { // Don't loop forever if Reader doesn't deliver EOF.
-			s.err = io.EOF
+			s.setErr(io.EOF)
 		}
 		s.end += n
 	}

src/pkg/bufio/scan_test.go

--- a/src/pkg/bufio/scan_test.go
+++ b/src/pkg/bufio/scan_test.go
@@ -368,3 +368,21 @@ func TestErrAtEOF(t *testing.T) {
 		t.Fatal("wrong error:", s.Err())
 	}
 }
+
+// Test for issue 5268.
+type alwaysError struct{}
+
+func (alwaysError) Read(p []byte) (int, error) {
+	return 0, io.ErrUnexpectedEOF
+}
+
+func TestNonEOFWithEmptyRead(t *testing.T) {
+	scanner := NewScanner(alwaysError{})
+	for scanner.Scan() {
+		t.Fatal("read should fail")
+	}
+	err := scanner.Err()
+	if err != io.ErrUnexpectedEOF {
+		t.Errorf("unexpected error: %v", err)
+	}
+}

コアとなるコードの解説

src/pkg/bufio/scan.go の変更

  1. Scan()メソッドのドキュメント更新: Scan()メソッドのコメントが更新され、falseを返す条件に「a zero-length read from the input」(入力からのゼロ長読み込み)が追加されました。これは、リーダーが0バイトを返した場合もスキャンが停止する可能性があることを明確にしています。

  2. エラー設定ロジックの変更: Scan()メソッド内のif n == 0 { ... }ブロックにおいて、s.err = io.EOFという直接代入がs.setErr(io.EOF)に変更されました。 この変更が重要です。s.setErr()メソッドは、既にエラーが設定されている場合(特にio.EOF以外のエラーが先に発生している場合)には、そのエラーを上書きしないロジックを持っています。これにより、io.Reader0, io.ErrUnexpectedEOFのような値を返した場合、まずs.setErr(err)によってio.ErrUnexpectedEOFs.errに設定され、その後のs.setErr(io.EOF)の呼び出しでは、既にエラーが設定されているためio.EOFで上書きされることがなくなります。結果として、Scanner.Err()io.ErrUnexpectedEOFを正しく返すようになります。

src/pkg/bufio/scan_test.go の変更

  1. alwaysError構造体の追加: io.Readerインターフェースを満たすalwaysErrorという新しい型が定義されました。この型は、Readメソッドが常に0, io.ErrUnexpectedEOFを返すように実装されています。これは、特定のシナリオ(リーダーが0バイトを返しつつ、io.EOF以外のエラーを返す場合)をシミュレートするために使用されます。

  2. TestNonEOFWithEmptyReadテスト関数の追加: この新しいテスト関数は、Issue 5268で報告された問題を再現し、修正が正しく機能することを確認します。

    • NewScanner(alwaysError{})で、常にio.ErrUnexpectedEOFを返すリーダーを持つScannerを作成します。
    • for scanner.Scan() { ... }ループは、Scan()falseを返すまで実行されます。alwaysErrorリーダーは常に0, io.ErrUnexpectedEOFを返すため、Scan()はすぐにfalseを返してループは実行されません。
    • err := scanner.Err()で、Scannerが保持しているエラーを取得します。
    • if err != io.ErrUnexpectedEOF { ... }で、取得したエラーが期待されるio.ErrUnexpectedEOFであるかどうかを検証します。もし修正が適用されていなければ、このテストは失敗し、errnilまたはio.EOFになってしまうでしょう。

これらの変更により、bufio.Scannerはより堅牢なエラーハンドリングを提供し、基となるリーダーから返されるエラーを正確に伝播できるようになりました。

関連リンク

  • Go言語のbufioパッケージドキュメント: https://pkg.go.dev/bufio
  • Go言語のioパッケージドキュメント: https://pkg.go.dev/io
  • Go言語のIssue 5268: bufio: Scanner.Err() returns nil for non-EOF errors (このコミットが修正した問題の報告) - GitHubのコミットページからリンクを辿るか、GoのIssueトラッカーで検索することで詳細を確認できます。

参考にした情報源リンク

  • GitHub: golang/go commit 082a4a8a47d03c5fd246b4d079391bdb21f2c3ed
  • Go言語の公式ドキュメント (pkg.go.dev)
  • Go言語のソースコード (特にsrc/pkg/bufio/scan.gosrc/pkg/bufio/scan_test.go)
  • Go言語のIssueトラッカー (Issue 5268に関する情報)