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

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

このコミットは、Go言語の標準ライブラリであるregexpパッケージ内のテストファイルexec_test.goにおいて、ファイルの読み込み方法をbufio.Readerからbufio.Scannerへ変更するものです。これにより、テストコードの可読性と堅牢性が向上しています。

コミット

このコミットは、regexpパッケージのテストコードexec_test.goにおいて、行単位のファイル読み込みにbufio.Readerの代わりにbufio.Scannerを使用するように変更しました。これにより、コードがより簡潔になり、エラーハンドリングが改善されています。

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

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

元コミット内容

regexp: use Scanner in exec_test

R=rsc
CC=golang-dev
https://golang.org/cl/7381046

変更の背景

この変更の背景には、Go言語におけるテキストファイルの行単位処理のベストプラクティスへの移行があります。bufio.Readerは低レベルなバッファリングI/Oを提供しますが、行単位で読み込む際には、ReadString('\n')を使用し、EOF(ファイルの終端)やその他のエラーを明示的にチェックし、改行文字を削除するなどの追加処理が必要でした。

一方、bufio.Scannerは、行、単語、またはカスタムの区切り文字でトークン化された入力を効率的かつイディオム的に読み取るために設計されています。Scan()メソッドとText()メソッドを組み合わせることで、行単位の処理が非常に簡潔に記述でき、エラーハンドリングもErr()メソッドでループ後にまとめて行えるため、コードの複雑さを軽減できます。

exec_test.goのようなテストファイルでは、簡潔で読みやすいコードが求められます。この変更は、テストコードの保守性を高め、将来的なGoのイディオムに沿った記述に統一することを目的としています。

前提知識の解説

bufioパッケージ

bufioパッケージは、I/O操作を効率化するためのバッファリング機能を提供します。これにより、ディスクI/OやネットワークI/Oの回数を減らし、パフォーマンスを向上させることができます。このパッケージには主にReaderScannerという2つの主要な型があります。

bufio.Reader

bufio.Readerは、より低レベルなバッファリングI/Oを提供します。バイト、ルーン、または区切り文字までの文字列を読み取るためのメソッドを提供します。行単位で読み込む場合、ReadString('\n')ReadLine()などのメソッドを使用します。

特徴:

  • 柔軟性: バイト単位、ルーン単位、文字列単位での読み込みが可能で、Peek()UnreadByte()のようなより詳細な制御が可能です。
  • エラーハンドリング: 各読み込みメソッドが直接エラーを返します。EOFもエラーとして扱われるため、明示的なチェックが必要です。
  • メモリ割り当て: ReadString()ReadBytes()は、呼び出しごとに新しい文字列やバイトスライスを割り当てるため、大量のデータを処理する場合には効率が低下する可能性があります。

行単位読み込みの例(変更前):

r := bufio.NewReader(txt)
for {
    line, err := r.ReadString('\n')
    if err != nil {
        if err == io.EOF {
            break
        }
        t.Fatalf("%s:%d: %v", file, lineno, err)
    }
    line = line[:len(line)-1] // chop \n
    lineno++
    // ... line processing ...
}

bufio.Scanner

bufio.Scannerは、入力ストリームをトークン(デフォルトでは行)に分割して読み取るための高レベルなインターフェースを提供します。特に、行単位や単語単位での読み込みに最適化されており、非常に簡潔なコードで記述できます。

特徴:

  • 簡潔性: Scan()メソッドで次のトークンに進み、Text()メソッドでそのトークンを文字列として取得できます。ループ構造が非常にシンプルになります。
  • 効率性: 内部でバッファを再利用するため、大量のデータを処理する際にメモリ割り当てのオーバーヘッドが少なくなります。
  • エラーハンドリング: Scan()メソッドはエラーが発生してもfalseを返し、ループ後にErr()メソッドでまとめてエラーを確認できます。EOFはScan()falseを返すことで自動的に処理されます。
  • トークンサイズ制限: デフォルトでトークンサイズに制限(64KB)がありますが、設定で変更可能です。

行単位読み込みの例(変更後):

scanner := bufio.NewScanner(txt)
for lineno := 1; scanner.Scan(); lineno++ {
    line := scanner.Text()
    // ... line processing ...
}
if err := scanner.Err(); err != nil {
    t.Fatalf("%s:%d: %v", file, lineno, err)
}

bufio.Readerbufio.Scannerの使い分け

  • bufio.Scannerを使用すべき場合:
    • 行単位、単語単位、またはシンプルなカスタム区切り文字でトークン化された入力を読み込む場合。
    • コードの簡潔さと効率性が重要であり、低レベルなI/O制御が不要な場合。
    • 一般的なテキスト処理タスク。
  • bufio.Readerを使用すべき場合:
    • より詳細なI/O制御(例: Peek()UnreadByte())が必要な場合。
    • バイナリデータや固定長レコードなど、トークン化が困難なデータを読み込む場合。
    • 非常に長い行(Scannerのバッファ制限を超える可能性のある行)を処理し、自身でバッファリングを管理したい場合。

このコミットでは、行単位のテキスト処理というbufio.Scannerが最も得意とするユースケースに合致するため、bufio.Readerからbufio.Scannerへの移行が行われました。

技術的詳細

exec_test.goファイルは、正規表現エンジンのテストケースを外部ファイルから読み込んで実行する役割を担っています。このテストでは、テストケースがファイル内に複数行にわたって記述されており、各行を個別に処理する必要があります。

変更前はbufio.ReaderReadString('\n')メソッドを使用していましたが、これにはいくつかの課題がありました。

  1. 明示的な改行文字の削除: ReadString('\n')は改行文字を含んだ文字列を返すため、line = line[:len(line)-1]のように手動で改行文字を削除する必要がありました。
  2. EOFの明示的なチェック: io.EOFエラーを明示的にチェックし、ループを終了させるロジックが必要でした。
  3. エラーハンドリングの複雑さ: ReadStringが返すエラーをその場で処理する必要があり、ループの制御構造が複雑になりがちでした。

bufio.Scannerへの変更により、これらの課題が解決されました。

  1. 自動的な改行文字の処理: scanner.Text()は改行文字を含まない文字列を返すため、手動での削除が不要になります。
  2. 簡潔なループ条件: for scanner.Scan()というループ条件により、次の行が存在するかどうかのチェックと、EOFの処理が自動的に行われます。
  3. 集約されたエラーハンドリング: ループ内でエラーを個別に処理する必要がなくなり、ループ終了後にscanner.Err()を呼び出すだけで、読み込み中に発生したエラーをまとめてチェックできます。これにより、エラー処理のロジックが簡潔になります。

この変更は、テストコードの堅牢性を損なうことなく、コードの量を減らし、読みやすさを向上させるという点で、Goのイディオムに沿った改善と言えます。

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

--- a/src/pkg/regexp/exec_test.go
+++ b/src/pkg/regexp/exec_test.go
@@ -89,7 +89,7 @@ func testRE2(t *testing.T, file string) {
 		txt = f
 	}
 	lineno := 0
-	r := bufio.NewReader(txt)
+	scanner := bufio.NewScanner(txt)
 	var (
 		str       []string
 		input     []string
@@ -99,16 +99,8 @@ func testRE2(t *testing.T, file string) {
 		nfail     int
 		ncase     int
 	)
-	for {
-		line, err := r.ReadString('\n')
-		if err != nil {
-			if err == io.EOF {
-				break
-			}
-			t.Fatalf("%s:%d: %v", file, lineno, err)
-		}
-		line = line[:len(line)-1] // chop \n
-		lineno++
+	for lineno := 1; scanner.Scan(); lineno++ {
+		line := scanner.Text()
 		switch {
 		case line == "":
 			t.Fatalf("%s:%d: unexpected blank line", file, lineno)
@@ -204,6 +196,9 @@ func testRE2(t *testing.T, file string) {
 		t.Fatalf("%s:%d: out of sync: %s\n", file, lineno, line)
 	}
+	if err := scanner.Err(); err != nil {
+		t.Fatalf("%s:%d: %v", file, lineno, err)
+	}
 	if len(input) != 0 {
 		t.Fatalf("%s:%d: out of sync: have %d strings left at EOF", file, lineno, len(input))
 	}

コアとなるコードの解説

  1. r := bufio.NewReader(txt) から scanner := bufio.NewScanner(txt) へ:

    • 変更前はbufio.NewReaderを使用してReaderインスタンスを作成していました。
    • 変更後はbufio.NewScannerを使用してScannerインスタンスを作成しています。これにより、行単位のトークン化に特化した機能を利用できるようになります。
  2. ループ構造の変更:

    • 変更前:
      for {
          line, err := r.ReadString('\n')
          if err != nil {
              if err == io.EOF {
                  break
              }
              t.Fatalf("%s:%d: %v", file, lineno, err)
          }
          line = line[:len(line)-1] // chop \n
          lineno++
          // ...
      }
      
      このループでは、無限ループfor {}の中でReadString('\n')を呼び出し、返されたエラーがio.EOFであればループを抜けるという明示的なエラーチェックとループ制御が必要でした。また、読み込んだ行から改行文字を削除する処理も必要でした。
    • 変更後:
      for lineno := 1; scanner.Scan(); lineno++ {
          line := scanner.Text()
          // ...
      }
      
      新しいループ構造はfor scanner.Scan()という非常に簡潔な形になっています。scanner.Scan()は次のトークン(この場合は次の行)が読み込めた場合にtrueを返し、EOFに達したりエラーが発生したりした場合はfalseを返します。これにより、EOFのチェックが不要になり、ループ条件自体が読み込みの成功を示します。 line := scanner.Text()は、読み込まれた行の内容を文字列として取得します。bufio.Scannerは自動的に改行文字を削除してくれるため、手動でのline = line[:len(line)-1]のような処理は不要になります。 lineno := 1は、行番号の初期化をループの開始時に行い、lineno++で各イテレーションで行番号をインクリメントしています。
  3. エラーハンドリングの追加:

    • 変更前:ReadString呼び出しの直後にエラーチェックが行われていました。
    • 変更後:
      if err := scanner.Err(); err != nil {
          t.Fatalf("%s:%d: %v", file, lineno, err)
      }
      
      ループが終了した後、scanner.Err()を呼び出すことで、読み込み中に発生した可能性のあるエラーをまとめてチェックしています。bufio.Scannerは、Scan()メソッドがfalseを返した理由がEOFではなく実際のエラーであった場合に、そのエラーを内部に保持し、Err()メソッドでそれを返します。これにより、エラー処理がループの外に集約され、コードがより整理されます。

これらの変更により、コードの行数が減り、ロジックがより明確になり、Goのイディオムに沿った形に改善されています。

関連リンク

参考にした情報源リンク

  • Go言語の公式ドキュメント (bufioパッケージ): https://pkg.go.dev/bufio
  • Go言語におけるbufio.Scannerbufio.Readerの比較に関する記事 (Web検索結果に基づく)