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

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

このコミットは、Go言語の標準ライブラリ text/scanner パッケージにおけるエラーハンドリングの改善に関するものです。具体的には、Scan メソッドが io.Reader からの読み込み時に io.EOF 以外のエラーを受け取った場合でも、読み込んだバイト数が0であればそのエラーを無視してしまう問題を修正しています。

コミット

commit b89a9fff5eee0eab8cb98d3c6532a8613dfdf580
Author: Rui Ueyama <ruiu@google.com>
Date:   Fri Mar 21 17:05:57 2014 -0700

    text/scanner: handle non-io.EOF errors
    
    Currently Scan ignores an error returned from source if the number
    of bytes source has read is 0.
    
    Fixes #7594.
    
    LGTM=gri
    R=golang-codereviews, bradfitz, gri
    CC=golang-codereviews
    https://golang.org/cl/78120043

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

https://github.com/golang/go/commit/b89a9fff5eee0eab8cb98d3c6532a8613dfdf580

元コミット内容

text/scanner パッケージの Scan メソッドが、ソース(io.Reader)からの読み込み時に、読み込んだバイト数が0であった場合に io.EOF 以外のエラーを無視してしまう問題を修正します。

変更の背景

text/scanner パッケージは、Go言語のソースコードやその他のテキストを字句解析(スキャン)するための機能を提供します。このパッケージは内部的に io.Reader インターフェースを介して入力ソースからデータを読み込みます。

Go言語の io.Reader インターフェースの Read メソッドの規約では、読み込みが成功したバイト数 n とエラー err を返します。重要な点として、Read メソッドは n > 0 であっても非nilのエラーを返すことが許されています(例: 部分的な読み込みの後にネットワークエラーが発生した場合など)。また、n = 0err != nil の場合、これは通常、読み込みが全く進まなかったことを示し、エラーが発生したことを意味します。特に、io.EOF は入力の終端に達したことを示す特別なエラーです。

このコミット以前の text/scanner の実装では、next() メソッド(内部で io.Reader から読み込みを行う)において、Read メソッドが n=0 かつ err != io.EOF のエラーを返した場合に、そのエラーが適切に処理されず、無視されてしまうというバグがありました。これにより、入力ソースが io.EOF ではない致命的なエラーを返しても、text/scanner はそれを検知せず、あたかも入力が終了したかのように振る舞う可能性がありました。これは、不正な入力や、基盤となるI/Oシステムの問題を適切に報告できないという点で、堅牢性に欠ける挙動でした。

コミットメッセージに記載されている Fixes #7594 は、この特定のバグを追跡していたGoのIssue番号ですが、現在の公開されているIssueトラッカーでは直接参照できない可能性があります(古いIssueであるか、内部的なトラッカーの番号であるため)。しかし、コミットメッセージ自体が問題の核心を明確に説明しています。

前提知識の解説

text/scanner パッケージ

text/scanner パッケージは、Go言語の標準ライブラリ go/scanner パッケージに似ていますが、より汎用的なテキストの字句解析を提供します。これは、プログラミング言語のパーサーや、設定ファイルの解析など、様々なテキスト処理タスクの基盤として利用できます。主な機能は以下の通りです。

  • トークン化: 入力ストリームを意味のある最小単位(トークン)に分割します。
  • 位置情報: 各トークンのソースコード上の正確な位置(行、列、オフセット)を追跡します。
  • エラーハンドリング: 字句解析中に発生したエラーを報告するためのメカニズムを提供します。

Scanner 型は Init メソッドで io.Reader を受け取り、そこから文字を読み込みます。Scan メソッドを呼び出すことで次のトークンを取得し、Next メソッドで次の文字を読み込みます。

io.Reader インターフェースとエラーハンドリング

Go言語の io.Reader インターフェースは、データを読み込むための基本的なインターフェースです。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Read メソッドは、p に最大 len(p) バイトを読み込み、読み込んだバイト数 n とエラー err を返します。

  • n > 0, err == nil: 正常に n バイト読み込んだ。
  • n > 0, err != nil: n バイト読み込んだが、その後にエラーが発生した(例: 部分的な読み込み)。この場合、n バイトは有効なデータとして扱われるべきです。
  • n = 0, err == nil: これは通常、有効なデータがないことを意味し、ブロックする可能性があるか、または Read が何も読み込まなかったことを示します。これは稀なケースであり、通常は Read がブロックするか、n > 0 を返すか、err != nil を返すかのいずれかです。
  • n = 0, err != nil: 読み込みが全く進まず、エラーが発生したことを意味します。
    • err == io.EOF: 入力の終端に達したことを示します。これはエラーではなく、正常な終了条件です。
    • err != io.EOF: io.EOF 以外のエラー(例: ネットワークエラー、ファイルシステムエラーなど)が発生し、読み込みが続行できないことを示します。これは通常、致命的なエラーとして扱われるべきです。

このコミットの修正は、特に n = 0 かつ err != io.EOF のケースに焦点を当てています。

技術的詳細

text/scanner パッケージの Scanner 型の next() メソッドは、内部的に入力ソースから文字を読み込む役割を担っています。このメソッドは、s.src (内部の io.Reader) からデータを s.srcBuf に読み込みます。

修正前のコードでは、s.src.Read(s.srcBuf[s.srcEnd:]) の呼び出し後、エラー err が発生した場合の処理に問題がありました。

// 修正前 (簡略化)
if err != nil {
    // ...
    if s.srcEnd == 0 { // 読み込んだバイト数が0の場合
        // ...
        if err != io.EOF { // io.EOF ではないエラーの場合
            s.error(err.Error()) // エラーハンドラを呼び出す
        }
        // ...
    }
    // ...
}

このロジックでは、s.srcEnd == 0 (つまり、Read が0バイトを返した) かつ err != io.EOF の場合にのみ s.error() が呼び出されていました。しかし、s.srcEndRead が返した n の値ではなく、バッファの現在の終端位置を示します。より正確には、nRead が実際に読み込んだバイト数であり、s.srcEnds.srcBuf 内の有効なデータの終端を指します。

問題は、Readn=0err != io.EOF を返した場合に、そのエラーが s.srcEnd == 0 の条件の外側で適切に処理されていなかった点にあります。つまり、s.srcEnd が0でなくても(以前の読み込みでバッファにデータが残っていたとしても)、現在の Read 呼び出しで n=0 かつ err != io.EOF であれば、それはエラーとして報告されるべきでした。

修正は、Read がエラーを返した直後に、n の値(s.srcEnd の増加量)に関わらず、io.EOF ではないエラーであればすぐに s.error() を呼び出すように変更しました。これにより、Read が0バイトを返したかどうかに関わらず、非 io.EOF エラーが即座に処理されるようになります。

新しいテストケース TestIOError は、この修正の意図を明確に示しています。このテストでは、Read メソッドが常に (0, io.ErrNoProgress) を返すカスタムの errReader を作成し、Scanner がこの非 io.EOF エラーを適切に捕捉し、エラーハンドラを呼び出すことを検証しています。

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

src/pkg/text/scanner/scanner.go

--- a/src/pkg/text/scanner/scanner.go
+++ b/src/pkg/text/scanner/scanner.go
@@ -240,12 +240,15 @@ func (s *Scanner) next() rune {
 		s.srcEnd = i + n
 		s.srcBuf[s.srcEnd] = utf8.RuneSelf // sentinel
 		if err != nil {
+			if err != io.EOF {
+				s.error(err.Error())
+			}
 			if s.srcEnd == 0 {
 				if s.lastCharLen > 0 {
 					// previous character was not EOF
 					s.error("I/O error")
 				}
 				s.lastCharLen = 0
 				return EOF
 			}
-			if err != io.EOF {
-				s.error(err.Error())
-			}
 			// If err == EOF, we won't be getting more
 			// bytes; break to avoid infinite loop. If
 			// err is something else, we don't know if

src/pkg/text/scanner/scanner_test.go

--- a/src/pkg/text/scanner/scanner_test.go
+++ b/src/pkg/text/scanner/scanner_test.go
@@ -462,6 +462,33 @@ func TestError(t *testing.T) {
 	testError(t, `/*/`, "1:4", "comment not terminated", EOF)
 }
 
+// An errReader returns (0, err) where err is not io.EOF.
+type errReader struct{}
+
+func (errReader) Read(b []byte) (int, error) {
+	return 0, io.ErrNoProgress // some error that is not io.EOF
+}
+
+func TestIOError(t *testing.T) {
+	s := new(Scanner).Init(errReader{})
+	errorCalled := false
+	s.Error = func(s *Scanner, msg string) {
+		if !errorCalled {
+			if want := io.ErrNoProgress.Error(); msg != want {
+				t.Errorf("msg = %q, want %q", msg, want)
+			}
+			errorCalled = true
+		}
+	}
+	tok := s.Scan()
+	if tok != EOF {
+		t.Errorf("tok = %s, want EOF", TokenString(tok))
+	}
+	if !errorCalled {
+		t.Errorf("error handler not called")
+	}
+}
+
 func checkPos(t *testing.T, got, want Position) {
 	if got.Offset != want.Offset || got.Line != want.Line || got.Column != want.Column {
 		t.Errorf("got offset, line, column = %d, %d, %d; want %d, %d, %d",

コアとなるコードの解説

src/pkg/text/scanner/scanner.go の変更点

Scanner.next() メソッド内のエラーハンドリングロジックが変更されました。

変更前: if err != nil ブロック内で、s.srcEnd == 0 (読み込んだバイト数が0) の場合にのみ、err != io.EOF であれば s.error(err.Error()) が呼び出されていました。

変更後: if err != nil ブロックに入った直後に、if err != io.EOF のチェックが追加されました。これにより、Read メソッドが io.EOF ではないエラーを返した場合、読み込んだバイト数 n が0であろうとなかろうと(ただし、この特定のケースでは ns.srcEnd に加算されるため、s.srcEnd が0でない場合でも n が0である可能性はあります)、すぐに s.error() メソッドが呼び出されるようになりました。

元の if s.srcEnd == 0 ブロック内の if err != io.EOF のチェックは削除されました。これは、新しいロジックがより包括的であるため、冗長になったためです。

この変更により、text/scannerio.Reader からの非 io.EOF エラーをより確実に捕捉し、ユーザー定義のエラーハンドラ (s.Error) を介して報告できるようになりました。

src/pkg/text/scanner/scanner_test.go の変更点

新しいテストケース TestIOError が追加されました。

  1. errReader 型の定義: io.Reader インターフェースを実装するカスタム型 errReader が定義されています。この Read メソッドは常に (0, io.ErrNoProgress) を返します。io.ErrNoProgressio.EOF ではないエラーの一例です。

  2. TestIOError 関数:

    • new(Scanner).Init(errReader{}) を使用して、errReader を入力ソースとする新しい Scanner インスタンスを作成します。
    • s.Error フィールドにカスタムのエラーハンドラ関数を設定します。このハンドラは、エラーが呼び出されたことを記録し、報告されたエラーメッセージが期待される io.ErrNoProgress.Error() と一致するかどうかを検証します。
    • s.Scan() を呼び出して、スキャン処理を開始します。errReader の性質上、すぐにエラーが発生し、EOF トークンが返されることが期待されます。
    • tok != EOF のチェックで、ScanEOF を返したことを確認します。
    • !errorCalled のチェックで、カスタムエラーハンドラが実際に呼び出されたことを確認します。

このテストは、text/scannerio.EOF ではない io.Reader からのエラーを正しく検出し、処理できることを保証します。

関連リンク

参考にした情報源リンク

  • Go言語のコミット履歴 (GitHub): https://github.com/golang/go/commits/master
  • Go言語の io.Reader インターフェースの規約に関する一般的な情報 (Goのドキュメントやブログ記事など)
  • Go言語の io.EOF とその他のエラーの区別に関する情報

(注: Fixes #7594 の具体的なIssueページは、公開されているGoのIssueトラッカーでは見つけることができませんでした。これは、Issueが非常に古いか、内部的なトラッカーの番号である可能性があります。しかし、コミットメッセージとコード変更から問題の性質は明確に理解できます。)