[インデックス 15333] ファイルの概要
このコミットは、Go言語の標準ライブラリ strconv
パッケージ内の浮動小数点数テスト (fp_test.go
) において、ファイルからの行読み込み処理を bufio.Reader
から bufio.Scanner
へと変更するものです。これにより、より効率的で慣用的な行単位の読み込みが実現され、コードが簡潔になっています。
コミット
commit f574371544f943b7280c1fdedac3220eccda9e86
Author: Rob Pike <r@golang.org>
Date: Wed Feb 20 13:38:19 2013 -0800
strconv: use Scanner in fp_test
R=rsc
CC=golang-dev
https://golang.org/cl/7385045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/f574371544f943b7280c1fdedac3220eccda9e86
元コミット内容
--- a/src/pkg/strconv/fp_test.go
+++ b/src/pkg/strconv/fp_test.go
@@ -7,7 +7,6 @@ package strconv_test
import (
"bufio"
"fmt"
- "io"
"os"
"strconv"
"strings"
@@ -102,19 +101,10 @@ func TestFp(t *testing.T) {
}\n\tdefer f.Close()\n \n-\tb := bufio.NewReader(f)\n+\ts := bufio.NewScanner(f)\n \n-\tlineno := 0\n-\tfor {\n-\t\tline, err2 := b.ReadString(\'\\n\')\n-\t\tif err2 == io.EOF {\n-\t\t\tbreak\n-\t\t}\n-\t\tif err2 != nil {\n-\t\t\tt.Fatal(\"testfp: read testdata/testfp.txt: \" + err2.Error())\n-\t\t}\n-\t\tline = line[0 : len(line)-1]\n-\t\tlineno++\n+\tfor lineno := 1; s.Scan(); lineno++ {\n+\t\tline := s.Text()\n \t\tif len(line) == 0 || line[0] == \'#\' {\n \t\t\tcontinue\n \t\t}\n@@ -148,4 +138,7 @@ func TestFp(t *testing.T) {\n \t\t\t\t\"want \", a[3], \" got \", s)\n \t\t}\n \t}\n+\tif s.Err() != nil {\n+\t\tt.Fatal(\"testfp: read testdata/testfp.txt: \", s.Err())\n+\t}\n }\n```
## 変更の背景
この変更の背景には、Go言語の標準ライブラリにおける `bufio.Scanner` の導入と、それが行単位のテキスト処理において `bufio.Reader` よりも推奨されるようになった経緯があります。
`bufio.Reader` は、バッファリングされたI/Oを提供し、`ReadString` や `ReadLine` といったメソッドで行を読み込むことができます。しかし、`ReadString` は区切り文字(この場合は改行文字 `\n`)を含む文字列を返し、`ReadLine` は改行文字を含まないバイトスライスを返しますが、どちらもエラーハンドリングや行末の処理(特にWindowsのCRLFなど)が煩雑になる傾向がありました。また、`io.EOF` のチェックとそれ以外のエラーの区別も手動で行う必要がありました。
一方、`bufio.Scanner` は、テキストやその他のプリミティブな値を読み取るためのシンプルなインターフェースを提供するために設計されました。特に、`Scan` メソッドと `Text` メソッドの組み合わせは、ファイルやストリームから行を読み込むためのイディオムとして推奨されています。`Scanner` は内部的にバッファリングを行い、改行文字の処理や `io.EOF` の検出を自動的に行い、よりクリーンなコードで読み込みループを記述できるようにします。
このコミットは、`strconv` パッケージのテストコードにおいて、より現代的で効率的、かつエラーハンドリングが容易な `bufio.Scanner` を採用することで、コードの可読性と保守性を向上させることを目的としています。テストコードは、ライブラリの正しい動作を保証するだけでなく、そのライブラリの適切な使用方法を示す良い例となるため、このような改善は重要です。
## 前提知識の解説
### Go言語の `bufio` パッケージ
`bufio` パッケージは、I/O操作をバッファリングすることで効率を向上させるための機能を提供します。これにより、ディスクやネットワークへのアクセス回数を減らし、プログラムのパフォーマンスを改善できます。
#### `bufio.Reader`
`bufio.Reader` は、`io.Reader` インターフェースをラップし、バッファリングされた読み込み機能を提供します。主なメソッドには以下のようなものがあります。
* `ReadString(delim byte) (string, error)`: 指定された区切り文字が見つかるまでデータを読み込み、その区切り文字を含む文字列を返します。エラーが発生した場合、または `io.EOF` に達した場合も文字列とエラーを返します。
* `ReadLine() (line []byte, isPrefix bool, err error)`: 次の行のデータを返します。行末の改行文字は含まれません。行がバッファに収まらない場合は `isPrefix` が `true` になります。
`ReadString` を使用して行を読み込む場合、返される文字列には改行文字が含まれるため、通常は `strings.TrimSuffix(line, "\n")` や `line[0:len(line)-1]` のようにして改行文字を取り除く必要があります。また、ループの終了条件として `err == io.EOF` を明示的にチェックする必要があります。
#### `bufio.Scanner`
`bufio.Scanner` は、`io.Reader` からデータを読み込み、それをトークン(デフォルトでは行)に分割するための高レベルなインターフェースを提供します。`Scanner` は、特にテキストファイルを1行ずつ処理するような場合に非常に便利です。
主なメソッドは以下の通りです。
* `Scan() bool`: 次のトークン(デフォルトでは次の行)が利用可能であれば `true` を返します。ストリームの終端に達したか、エラーが発生した場合は `false` を返します。
* `Text() string`: `Scan` が `true` を返した場合に、現在のトークン(行)の内容を文字列として返します。この文字列には、デフォルトの分割関数(`ScanLines`)を使用している場合、行末の改行文字は含まれません。
* `Bytes() []byte`: `Text()` と同様に、現在のトークンの内容をバイトスライスとして返します。
* `Err() error`: `Scan` が `false` を返した原因となったエラーを返します。ストリームの終端に達した場合は `nil` を返します。
`Scanner` を使用すると、読み込みループは `for s.Scan() { ... }` のように非常に簡潔に記述でき、エラーチェックはループの後に `s.Err() != nil` で一度に行うことができます。
### `io.EOF`
`io.EOF` は、`io` パッケージで定義されているエラー変数で、入力の終端(End Of File)に達したことを示します。ファイルやネットワークストリームからの読み込み操作で、これ以上データがない場合に返されます。`bufio.Reader` を使用する場合、このエラーを明示的にチェックしてループを終了させる必要があります。
### `testing` パッケージと `t.Fatal()`
Go言語の標準ライブラリには、テストを記述するための `testing` パッケージが含まれています。
* `*testing.T`: 各テスト関数に渡される型で、テストの状態を管理し、テストの失敗を報告するためのメソッドを提供します。
* `t.Fatal(args ...interface{})`: テストを失敗としてマークし、現在のテスト関数の実行を停止します。引数は `fmt.Println` と同様にフォーマットされて出力されます。`t.Fatalf` はフォーマット文字列を指定できます。
### `strconv` パッケージ
`strconv` パッケージは、基本的なデータ型(文字列、整数、浮動小数点数、真偽値)間の変換機能を提供します。例えば、文字列を数値に変換したり、数値を文字列に変換したりする際に使用されます。このコミットが変更している `fp_test.go` は、このパッケージの浮動小数点数変換機能のテストコードです。
## 技術的詳細
このコミットの主要な技術的変更点は、ファイルからの行読み込みロジックを `bufio.Reader` から `bufio.Scanner` へと移行したことです。
**変更前 (`bufio.Reader` を使用):**
```go
b := bufio.NewReader(f)
lineno := 0
for {
line, err2 := b.ReadString('\n')
if err2 == io.EOF {
break
}
if err2 != nil {
t.Fatal("testfp: read testdata/testfp.txt: " + err2.Error())
}
line = line[0 : len(line)-1] // 改行文字の除去
lineno++
// ... 処理 ...
}
このコードでは、以下の点に注意が必要です。
b.ReadString('\n')
は、改行文字を含む文字列を返します。そのため、line = line[0 : len(line)-1]
で明示的に改行文字を取り除く必要があります。これは、Windows環境でのCRLF (\r\n
) の場合、さらに複雑になる可能性があります(ただし、このテストデータではLFのみを想定している可能性が高い)。- ループの終了条件として
err2 == io.EOF
をチェックし、それ以外のエラーはt.Fatal
で処理しています。io.EOF
はエラーではないため、特別な処理が必要です。 - 行番号
lineno
は手動でインクリメントしています。
変更後 (bufio.Scanner
を使用):
s := bufio.NewScanner(f)
for lineno := 1; s.Scan(); lineno++ {
line := s.Text()
// ... 処理 ...
}
if s.Err() != nil {
t.Fatal("testfp: read testdata/testfp.txt: ", s.Err())
}
この変更により、コードは大幅に簡潔になり、以下の利点が得られます。
s.Scan()
メソッドが、次の行が読み込めるかどうかを判断し、ストリームの終端やエラーを自動的に処理します。ループ条件がs.Scan()
だけで済むため、非常に読みやすいです。s.Text()
メソッドは、デフォルトの分割関数 (bufio.ScanLines
) を使用している場合、行末の改行文字を含まない文字列を返します。これにより、手動での改行文字除去が不要になります。- エラーハンドリングが簡素化されます。ループ内で
io.EOF
を個別にチェックする必要がなくなり、ループ終了後にs.Err()
を一度チェックするだけで、読み込み中に発生したエラーを捕捉できます。s.Err()
はio.EOF
の場合はnil
を返すため、エラーと終端の区別が自動的に行われます。 - 行番号
lineno
のインクリメントがfor
ループの初期化と後処理の部分に統合され、よりGoらしいイディオムになっています。
この変更は、Go言語におけるテキスト処理のベストプラクティスへの移行を示しており、より堅牢で保守しやすいコードにつながります。
コアとなるコードの変更箇所
--- a/src/pkg/strconv/fp_test.go
+++ b/src/pkg/strconv/fp_test.go
@@ -7,7 +7,6 @@ package strconv_test
import (
"bufio"
"fmt"
- "io" // ioパッケージのインポートが削除された
"os"
"strconv"
"strings"
@@ -102,19 +101,10 @@ func TestFp(t *testing.T) {
}\n\tdefer f.Close()\n \n-\tb := bufio.NewReader(f) // bufio.Readerのインスタンス化が削除された
+\ts := bufio.NewScanner(f) // bufio.Scannerのインスタンス化が追加された
\n-\tlineno := 0 // 行番号の初期化が削除された
-\tfor { // 無限ループと手動でのエラー・EOFチェックが削除された
-\t\tline, err2 := b.ReadString(\'\\n\')
-\t\tif err2 == io.EOF {
-\t\t\tbreak
-\t\t}\n-\t\tif err2 != nil {
-\t\t\tt.Fatal(\"testfp: read testdata/testfp.txt: \" + err2.Error())
-\t\t}\n-\t\tline = line[0 : len(line)-1] // 改行文字の除去が削除された
-\t\tlineno++ // 行番号のインクリメントが削除された
+\tfor lineno := 1; s.Scan(); lineno++ { // bufio.Scannerを使った新しいループ構造
+\t\tline := s.Text() // Scannerから行の内容を取得
\t\tif len(line) == 0 || line[0] == \'#\' {
\t\t\tcontinue
\t\t}\n@@ -148,4 +138,7 @@ func TestFp(t *testing.T) {\n \t\t\t\t\"want \", a[3], \" got \", s)\n \t\t}\n \t}\n+\tif s.Err() != nil { // ループ後のエラーチェックが追加された
+\t\tt.Fatal(\"testfp: read testdata/testfp.txt: \", s.Err())\n+\t}\n }\n```
## コアとなるコードの解説
このコミットのコアとなる変更は、`TestFp` 関数内のファイル読み込みループです。
1. **`io` パッケージの削除**: `bufio.Scanner` は `io.EOF` を直接扱う必要がないため、`io` パッケージのインポートが不要になりました。
2. **`bufio.Reader` から `bufio.Scanner` への移行**:
* `b := bufio.NewReader(f)` が削除され、代わりに `s := bufio.NewScanner(f)` が追加されました。これにより、ファイル `f` からの読み込みに `Scanner` が使用されるようになります。
3. **読み込みループの変更**:
* 変更前は `for {}` の無限ループ内で `b.ReadString('\n')` を呼び出し、返されたエラーが `io.EOF` かどうかでループを終了させていました。また、読み込んだ行から手動で改行文字を削除していました (`line = line[0 : len(line)-1]`)。
* 変更後は `for lineno := 1; s.Scan(); lineno++` という簡潔なループになりました。
* `s.Scan()` は、次の行が読み込める場合に `true` を返し、読み込めない場合(EOFまたはエラー)に `false` を返します。これにより、ループの条件が非常にシンプルになります。
* `lineno := 1; ...; lineno++` は、Goの `for` ループの初期化、条件、後処理の構文を効果的に利用して行番号を管理しています。
* `line := s.Text()` は、改行文字を含まないクリーンな行文字列を返します。
4. **エラーハンドリングの変更**:
* 変更前はループ内で `err2 != nil` かつ `err2 != io.EOF` の場合に `t.Fatal` を呼び出していました。
* 変更後は、ループの後に `if s.Err() != nil { t.Fatal(...) }` という形で、読み込み中に発生したエラーをまとめてチェックするようになりました。`s.Err()` は `io.EOF` の場合は `nil` を返すため、この単一のチェックで十分です。
これらの変更により、コードはよりGoのイディオムに沿ったものとなり、可読性、保守性、そして堅牢性が向上しています。
## 関連リンク
* Go言語 `bufio` パッケージのドキュメント: [https://pkg.go.dev/bufio](https://pkg.go.dev/bufio)
* Go言語 `bufio.Scanner` のドキュメント: [https://pkg.go.dev/bufio#Scanner](https://pkg.go.dev/bufio#Scanner)
* Go言語 `bufio.Reader` のドキュメント: [https://pkg.go.dev/bufio#Reader](https://pkg.go.dev/bufio#Reader)
* Go言語 `io` パッケージのドキュメント: [https://pkg.go.dev/io](https://pkg.go.dev/io)
* Go言語 `testing` パッケージのドキュメント: [https://pkg.go.dev/testing](https://pkg.go.dev/testing)
* Go言語 `strconv` パッケージのドキュメント: [https://pkg.go.dev/strconv](https://pkg.go.dev/strconv)
## 参考にした情報源リンク
* Go言語の公式ドキュメント (上記関連リンクに記載)
* Go言語の `bufio.Scanner` と `bufio.Reader` の比較に関する一般的な情報源 (例: Go言語のブログ記事、技術記事、Stack Overflowなど)
* (具体的なURLはWeb検索結果によるため、ここでは一般的な説明に留めますが、実際の作業では検索結果から適切なものを選択します。)
* 例: "Go bufio.Scanner vs bufio.Reader" で検索し、信頼できる情報源を参照。
* A Tour of Go: [https://go.dev/tour/](https://go.dev/tour/)
* Go by Example: [https://gobyexample.com/](https://gobyexample.com/)
* The Go Programming Language Specification: [https://go.dev/ref/spec](https://go.dev/ref/spec)
* Effective Go: [https://go.dev/doc/effective_go](https://go.dev/doc/effective_go)
* Go Blog: [https://go.dev/blog/](https://go.dev/blog/)
* Stack Overflow: [https://stackoverflow.com/](https://stackoverflow.com/)
* Go Forum: [https://forum.golang.org/](https://forum.golang.org/)