[インデックス 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の回数を減らし、パフォーマンスを向上させることができます。このパッケージには主にReader
とScanner
という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.Reader
とbufio.Scanner
の使い分け
bufio.Scanner
を使用すべき場合:- 行単位、単語単位、またはシンプルなカスタム区切り文字でトークン化された入力を読み込む場合。
- コードの簡潔さと効率性が重要であり、低レベルなI/O制御が不要な場合。
- 一般的なテキスト処理タスク。
bufio.Reader
を使用すべき場合:- より詳細なI/O制御(例:
Peek()
、UnreadByte()
)が必要な場合。 - バイナリデータや固定長レコードなど、トークン化が困難なデータを読み込む場合。
- 非常に長い行(
Scanner
のバッファ制限を超える可能性のある行)を処理し、自身でバッファリングを管理したい場合。
- より詳細なI/O制御(例:
このコミットでは、行単位のテキスト処理というbufio.Scanner
が最も得意とするユースケースに合致するため、bufio.Reader
からbufio.Scanner
への移行が行われました。
技術的詳細
exec_test.go
ファイルは、正規表現エンジンのテストケースを外部ファイルから読み込んで実行する役割を担っています。このテストでは、テストケースがファイル内に複数行にわたって記述されており、各行を個別に処理する必要があります。
変更前はbufio.Reader
のReadString('\n')
メソッドを使用していましたが、これにはいくつかの課題がありました。
- 明示的な改行文字の削除:
ReadString('\n')
は改行文字を含んだ文字列を返すため、line = line[:len(line)-1]
のように手動で改行文字を削除する必要がありました。 - EOFの明示的なチェック:
io.EOF
エラーを明示的にチェックし、ループを終了させるロジックが必要でした。 - エラーハンドリングの複雑さ:
ReadString
が返すエラーをその場で処理する必要があり、ループの制御構造が複雑になりがちでした。
bufio.Scanner
への変更により、これらの課題が解決されました。
- 自動的な改行文字の処理:
scanner.Text()
は改行文字を含まない文字列を返すため、手動での削除が不要になります。 - 簡潔なループ条件:
for scanner.Scan()
というループ条件により、次の行が存在するかどうかのチェックと、EOFの処理が自動的に行われます。 - 集約されたエラーハンドリング: ループ内でエラーを個別に処理する必要がなくなり、ループ終了後に
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))
}
コアとなるコードの解説
-
r := bufio.NewReader(txt)
からscanner := bufio.NewScanner(txt)
へ:- 変更前は
bufio.NewReader
を使用してReader
インスタンスを作成していました。 - 変更後は
bufio.NewScanner
を使用してScanner
インスタンスを作成しています。これにより、行単位のトークン化に特化した機能を利用できるようになります。
- 変更前は
-
ループ構造の変更:
- 変更前:
このループでは、無限ループ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++
で各イテレーションで行番号をインクリメントしています。
- 変更前:
-
エラーハンドリングの追加:
- 変更前: 各
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 CL (Code Review) 7381046: https://golang.org/cl/7381046
参考にした情報源リンク
- Go言語の公式ドキュメント (
bufio
パッケージ): https://pkg.go.dev/bufio - Go言語における
bufio.Scanner
とbufio.Reader
の比較に関する記事 (Web検索結果に基づく)