[インデックス 15330] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージに、新しいScanner
インターフェースを導入するものです。これにより、テキストデータのスキャン(読み込みと解析)がより簡単かつ効率的に行えるようになります。また、strings
パッケージのFields
関数におけるスペースの定義も改善されています。
コミット
commit 55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
Author: Rob Pike <r@golang.org>
Date: Wed Feb 20 12:14:31 2013 -0800
bufio: new Scanner interface
Add a new, simple interface for scanning (probably textual) data,
based on a new type called Scanner. It does its own internal buffering,
so should be plausibly efficient even without injecting a bufio.Reader.
The format of the input is defined by a "split function", by default
splitting into lines. Other implemented split functions include single
bytes, single runes, and space-separated words.
Here's the loop to scan stdin as a file of lines:
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Printf("%s\n", s.Bytes())
}\n if s.Err() != nil {
log.Fatal(s.Err())
}
While we're dealing with spaces, define what space means to strings.Fields.
Fixes #4802.
R=adg, rogpeppe, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/7322088
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
元コミット内容
bufio: new Scanner interface
新しいScanner
型に基づいた、テキストデータのスキャン(おそらくテキストデータ)のための新しいシンプルなインターフェースを追加します。これは独自の内部バッファリングを行うため、bufio.Reader
を注入しなくても十分に効率的であるはずです。入力の形式は「分割関数(split function)」によって定義され、デフォルトでは行に分割されます。その他の実装された分割関数には、単一バイト、単一ルーン、スペース区切りの単語が含まれます。
標準入力を複数行のファイルとしてスキャンするループの例を以下に示します。
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Printf("%s\n", s.Bytes())
}
if s.Err() != nil {
log.Fatal(s.Err())
}
スペースを扱っている間、strings.Fields
にとってスペースが何を意味するのかを定義します。
Fixes #4802.
変更の背景
この変更の主な背景は、Go言語におけるテキストデータの読み込みと解析をより簡単かつ堅牢にするための新しいメカニズムを提供することです。既存のbufio.Reader
は低レベルな操作には適していましたが、行単位や単語単位といった一般的なテキスト処理には、より多くのボイラープレートコードが必要でした。
具体的には、コミットメッセージに記載されているFixes #4802
がこの変更の直接的なトリガーとなっています。Issue #4802は「bufio
: ReadString
とReadLine
の代替として、より良い行スキャナーが必要」という内容で、bufio.Reader
のReadLine
メソッドが返すバイトスライスが次の読み込みで上書きされる可能性があり、安全でないという問題提起がされていました。また、ReadString
はメモリ効率が悪いという指摘もありました。
Scanner
インターフェースの導入により、以下の点が改善されます。
- 使いやすさ:
Scan()
メソッドをループで回すだけで、簡単にトークン(行、単語など)を順次取得できるようになります。 - 安全性:
Bytes()
メソッドが返すスライスは、次のScan()
呼び出しまで有効であることが保証され、ReadLine
のような上書きの問題がありません。Text()
メソッドは新しい文字列を返すため、さらに安全です。 - 柔軟性:
SplitFunc
という概念を導入することで、ユーザーが独自の分割ロジックを簡単に定義できるようになり、行、単語、バイト、ルーンといった標準的な分割方法だけでなく、様々な形式のテキストデータに対応できます。 - 効率性: 内部でバッファリングを行うため、
bufio.Reader
を明示的に使用しなくても効率的な読み込みが可能です。
また、strings.Fields
の変更は、bufio.Scanner
がスペースの定義にunicode.IsSpace
を使用するようになったことに合わせて、一貫性を持たせるためのものです。これにより、Go言語全体で「スペース」の解釈が統一され、予期せぬ挙動を防ぐことができます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念と標準ライブラリの知識が必要です。
io.Reader
インターフェース: Go言語におけるデータの読み込み元を抽象化するインターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
にデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。os.Stdin
やbytes.Buffer
、ファイルなどがこのインターフェースを実装しています。bufio
パッケージ: バッファリングされたI/O操作を提供するパッケージです。bufio.Reader
は、io.Reader
をラップしてバッファリングを行い、効率的な読み込みを可能にします。strings
パッケージ: 文字列操作に関するユーティリティ関数を提供するパッケージです。strings.Fields
は、文字列をスペースで分割して単語のリストを返します。unicode
パッケージ: Unicode文字に関するプロパティ(例えば、文字が数字か、空白文字かなど)を扱うための関数を提供するパッケージです。unicode.IsSpace
は、与えられたルーンがUnicodeの空白文字であるかどうかを判定します。- ルーン (Rune): Go言語では、Unicodeコードポイントを表現するために
rune
型(int32
のエイリアス)を使用します。文字列はバイトのシーケンスですが、UTF-8エンコーディングされたルーンのシーケンスとして扱うことができます。 - UTF-8: Unicode文字をバイト列にエンコードするための可変長エンコーディング方式です。Go言語の文字列は内部的にUTF-8でエンコードされています。
- エラーハンドリング: Go言語では、関数がエラーを返す場合、通常は戻り値の最後の要素として
error
型の値を返します。呼び出し元は、このエラー値をチェックして適切な処理を行います。io.EOF
は、入力の終端に達したことを示す特別なエラーです。
技術的詳細
bufio.Scanner
の設計思想
bufio.Scanner
は、ストリームから「トークン」を抽出するための高レベルなインターフェースを提供します。その核心は、以下の3つの主要な要素にあります。
-
Scanner
構造体:r io.Reader
: 入力元となるio.Reader
。split SplitFunc
: トークンを分割するための関数。maxTokenSize int
: 許容されるトークンの最大サイズ。これを超えるとErrTooLong
エラーが発生します。token []byte
: 直前にスキャンされたトークンのバイトスライス。buf []byte
: 内部バッファ。io.Reader
から読み込んだデータを一時的に保持します。start int
,end int
:buf
内の有効なデータの範囲を示すインデックス。err error
: スキャン中に発生した最初のエラーを保持します。
-
SplitFunc
型:type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
この関数は、Scanner
の心臓部であり、入力データdata
からトークンを識別し、そのトークンが消費したバイト数advance
、抽出されたトークンtoken
、および発生したエラーerr
を返します。data []byte
: 現在処理中の入力データのスライス。atEOF bool
: 入力ストリームがEOFに達しているかどうかを示すフラグ。advance int
:data
スライス内で、次のスキャンを開始するためにスキップすべきバイト数。token []byte
: 抽出されたトークン。err error
: 分割中に発生したエラー。
SplitFunc
が(0, nil, nil)
を返した場合、それは「現在利用可能なデータでは完全なトークンを形成できないため、さらにデータを読み込む必要がある」ことをScanner
に伝えます。 -
Scan()
メソッド:func (s *Scanner) Scan() bool
このメソッドがScanner
の主要な操作です。Scan()
が呼び出されるたびに、SplitFunc
を使用して次のトークンを抽出しようとします。- 内部バッファに十分なデータがない場合、
s.r.Read()
を呼び出してデータを補充します。 - バッファが最大トークンサイズを超えそうになった場合、バッファを拡張します。
SplitFunc
がトークンを返した場合、s.token
に設定し、true
を返します。- EOFに達するか、I/Oエラーまたは
SplitFunc
からのエラーが発生した場合、false
を返します。
- 内部バッファに十分なデータがない場合、
デフォルトのSplitFunc
の実装
このコミットでは、いくつかの便利なSplitFunc
が提供されています。
-
ScanLines
:- 行末の
\r?\n
(CRLFまたはLF)を削除して行を返します。 - 最後の行に改行がない場合でも、その行をトークンとして返します。
bytes.IndexByte(data, '\n')
を使用して改行文字を探します。dropCR
ヘルパー関数で末尾の\r
を削除します。
- 行末の
-
ScanBytes
:- 入力の各バイトを個別のトークンとして返します。
advance
は常に1
です。
-
ScanRunes
:- 入力の各UTF-8エンコードされたルーンをトークンとして返します。
- 不正なUTF-8シーケンスは
U+FFFD
(utf8.RuneError
)に変換されますが、advance
は1バイトのみ進みます。これは、Goの文字列に対するrange
ループの挙動と一致します。 utf8.DecodeRune
とutf8.FullRune
を使用してルーンのデコードと完全性のチェックを行います。
-
ScanWords
:- スペースで区切られた単語をトークンとして返します。
- 単語の定義には
isSpace
関数(後述)を使用し、unicode.IsSpace
と互換性があります。 - 先頭と末尾のスペースはスキップされます。
strings.Fields
の変更
strings.Fields
関数は、文字列を1つ以上の連続する空白文字で分割する関数です。このコミットでは、その空白文字の定義がunicode.IsSpace
によって定義されるように変更されました。
変更前: 内部的なASCIIベースの空白文字判定
変更後: unicode.IsSpace
に基づくUnicodeの空白文字判定
これにより、strings.Fields
とbufio.Scanner
のScanWords
が、より広範なUnicode空白文字(例えば、ノーブレークスペース\u00A0
や全角スペース\u3000
など)に対して一貫した挙動を示すようになります。
エラー処理
Scanner
は、以下のエラーを定義しています。
ErrTooLong
: トークンがmaxTokenSize
を超えた場合に発生します。ErrNegativeAdvance
:SplitFunc
が負のadvance
値を返した場合に発生します。ErrAdvanceTooFar
:SplitFunc
がdata
スライスの範囲を超えるadvance
値を返した場合に発生します。
Scan()
メソッドがfalse
を返した後、Err()
メソッドを呼び出すことで、スキャン中に発生したエラー(io.EOF
を除く)を取得できます。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の通りです。
src/pkg/bufio/bufio_test.go
: 既存のbufio
テストファイル。TestNegativeRead
関数内のt.Fatal
がt.Fatalf
に変更されています。これは小さな修正ですが、テストの出力形式を改善するものです。src/pkg/bufio/export_test.go
: 新規追加されたファイル。テスト目的でbufio
パッケージの内部関数やフィールドをエクスポートします。IsSpace
(isSpace
のテスト用)、Scanner.MaxTokenSize
(テストでバッファサイズを制御するため)、Scanner.ErrOrEOF
(特定のテストケース用)が含まれます。src/pkg/bufio/scan.go
: このコミットの主要な変更点。Scanner
型、SplitFunc
型、関連するエラー変数、NewScanner
コンストラクタ、Scan
メソッド、Bytes
、Text
、Err
、Split
メソッド、そしてデフォルトのSplitFunc
実装(ScanBytes
,ScanRunes
,ScanLines
,ScanWords
)が定義されています。src/pkg/bufio/scan_test.go
: 新規追加されたテストファイル。bufio.Scanner
の新しい機能(ScanBytes
,ScanRunes
,ScanWords
,ScanLines
)を網羅的にテストします。特に、TestSpace
はisSpace
関数がunicode.IsSpace
と一致するかどうかを検証しています。また、長い行の処理、改行なしの行、エラー処理など、様々なエッジケースがテストされています。src/pkg/strings/strings.go
:Fields
関数のコメントが更新され、unicode.IsSpace
を使用することが明記されました。また、Fields
関数自体もFieldsFunc(s, unicode.IsSpace)
を呼び出すように変更され、unicode
パッケージへの依存が導入されました。
コアとなるコードの解説
src/pkg/bufio/scan.go
Scanner
構造体
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
}
Scanner
の内部状態を管理する構造体です。r
は入力元、split
は分割ロジック、buf
は内部バッファ、token
は直前の結果を保持します。
Scan()
メソッド
func (s *Scanner) Scan() bool {
// Loop until we have a token.
for {
// See if we can get a token with what we already have.
if s.end > s.start {
advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
if err != nil {
s.setErr(err)
return false
}
if !s.advance(advance) {
return false
}
s.token = token
if token != nil {
return true
}
}
// ... (buffer management and reading more data) ...
}
panic("not reached")
}
Scan()
メソッドは、Scanner
の主要なループを実装しています。
- まず、現在のバッファ内のデータでトークンを生成できるか
s.split
関数を呼び出して試みます。 s.split
がエラーを返した場合、そのエラーを記録し、false
を返してスキャンを停止します。s.split
がトークンを返した場合、s.token
に設定し、true
を返します。- トークンが生成できない場合(
s.split
が(0, nil, nil)
を返した場合)、またはバッファが空の場合、io.Reader
からさらにデータを読み込み、バッファを拡張するなどの処理を行います。
SplitFunc
の例: ScanLines
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, dropCR(data[0:i]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
ScanLines
は、行単位でデータを分割するSplitFunc
の典型的な実装です。
data
中に改行文字\n
が見つかった場合、その位置までをトークンとし、改行文字を含めてadvance
します。dropCR
で末尾の\r
を削除します。atEOF
がtrue
で、かつdata
が空でない場合(つまり、入力の終端に達し、かつ最後の行に改行がない場合)、残りのdata
全体をトークンとして返します。- それ以外の場合(改行が見つからず、EOFでもない場合)、
(0, nil, nil)
を返して、Scanner
にさらにデータを読み込むように要求します。
isSpace
関数
func isSpace(r rune) bool {
if r <= '\u00FF' {
// Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
switch r {
case ' ', '\t', '\n', '\v', '\f', '\r':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
// High-valued ones.
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u180e', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}
isSpace
は、与えられたルーンが空白文字であるかを判定する内部関数です。unicode
パッケージに依存しないように、手動で一般的なASCIIおよび一部のUnicode空白文字をチェックしています。scan_test.go
のTestSpace
で、この関数がunicode.IsSpace
と一致するかどうかが検証されています。
src/pkg/strings/strings.go
Fields
関数の変更
func Fields(s string) []string {
return FieldsFunc(s, unicode.IsSpace)
}
strings.Fields
関数は、以前は内部的なロジックで空白文字を判定していましたが、このコミットでunicode.IsSpace
を引数としてFieldsFunc
を呼び出すように変更されました。これにより、strings.Fields
の空白文字の定義がUnicodeの標準に準拠し、bufio.Scanner
のScanWords
と一貫性を持つようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
- Go Issue #4802: https://code.google.com/p/go/issues/detail?id=4802 (現在はGitHubに移行済み: https://github.com/golang/go/issues/4802)
- Go CL 7322088: https://golang.org/cl/7322088
参考にした情報源リンク
- Go言語の公式ドキュメント (
bufio
パッケージ): https://pkg.go.dev/bufio - Go言語の公式ドキュメント (
strings
パッケージ): https://pkg.go.dev/strings - Go言語の公式ドキュメント (
unicode
パッケージ): https://pkg.go.dev/unicode - Go言語の公式ドキュメント (
io
パッケージ): https://pkg.go.dev/io - Go言語の公式ブログ: https://go.dev/blog/ (特定の記事は直接参照していませんが、Goの設計思想を理解する上で役立ちます)
- Stack Overflowなどのコミュニティサイト (一般的なGoのテキスト処理や
bufio
に関する議論) - Go言語のソースコード (特に
src/pkg/bufio/scan.go
とsrc/pkg/strings/strings.go
の変更履歴)
[インデックス 15330] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージに、新しいScanner
インターフェースを導入するものです。これにより、テキストデータのスキャン(読み込みと解析)がより簡単かつ効率的に行えるようになります。また、strings
パッケージのFields
関数におけるスペースの定義も改善されています。
コミット
commit 55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
Author: Rob Pike <r@golang.org>
Date: Wed Feb 20 12:14:31 2013 -0800
bufio: new Scanner interface
Add a new, simple interface for scanning (probably textual) data,
based on a new type called Scanner. It does its own internal buffering,
so should be plausibly efficient even without injecting a bufio.Reader.
The format of the input is defined by a "split function", by default
splitting into lines. Other implemented split functions include single
bytes, single runes, and space-separated words.
Here's the loop to scan stdin as a file of lines:
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Printf("%s\n", s.Bytes())
}
if s.Err() != nil {
log.Fatal(s.Err())
}
While we're dealing with spaces, define what space means to strings.Fields.
Fixes #4802.
R=adg, rogpeppe, bradfitz, rsc
CC=golang-dev
https://golang.org/cl/7322088
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
元コミット内容
bufio: new Scanner interface
新しいScanner
型に基づいた、テキストデータのスキャン(おそらくテキストデータ)のための新しいシンプルなインターフェースを追加します。これは独自の内部バッファリングを行うため、bufio.Reader
を注入しなくても十分に効率的であるはずです。入力の形式は「分割関数(split function)」によって定義され、デフォルトでは行に分割されます。その他の実装された分割関数には、単一バイト、単一ルーン、スペース区切りの単語が含まれます。
標準入力を複数行のファイルとしてスキャンするループの例を以下に示します。
s := bufio.NewScanner(os.Stdin)
for s.Scan() {
fmt.Printf("%s\n", s.Bytes())
}
if s.Err() != nil {
log.Fatal(s.Err())
}
スペースを扱っている間、strings.Fields
にとってスペースが何を意味するのかを定義します。
Fixes #4802.
変更の背景
この変更の主な背景は、Go言語におけるテキストデータの読み込みと解析をより簡単かつ堅牢にするための新しいメカニズムを提供することです。既存のbufio.Reader
は低レベルな操作には適していましたが、行単位や単語単位といった一般的なテキスト処理には、より多くのボイラープレートコードが必要でした。
具体的には、コミットメッセージに記載されているFixes #4802
がこの変更の直接的なトリガーとなっています。Issue #4802は「bufio
: ReadString
とReadLine
の代替として、より良い行スキャナーが必要」という内容で、bufio.Reader
のReadLine
メソッドが返すバイトスライスが次の読み込みで上書きされる可能性があり、安全でないという問題提起がされていました。また、ReadString
はメモリ効率が悪いという指摘もありました。
Scanner
インターフェースの導入により、以下の点が改善されます。
- 使いやすさ:
Scan()
メソッドをループで回すだけで、簡単にトークン(行、単語など)を順次取得できるようになります。 - 安全性:
Bytes()
メソッドが返すスライスは、次のScan()
呼び出しまで有効であることが保証され、ReadLine
のような上書きの問題がありません。Text()
メソッドは新しい文字列を返すため、さらに安全です。 - 柔軟性:
SplitFunc
という概念を導入することで、ユーザーが独自の分割ロジックを簡単に定義できるようになり、行、単語、バイト、ルーンといった標準的な分割方法だけでなく、様々な形式のテキストデータに対応できます。 - 効率性: 内部でバッファリングを行うため、
bufio.Reader
を明示的に使用しなくても効率的な読み込みが可能です。
また、strings.Fields
の変更は、bufio.Scanner
がスペースの定義にunicode.IsSpace
を使用するようになったことに合わせて、一貫性を持たせるためのものです。これにより、Go言語全体で「スペース」の解釈が統一され、予期せぬ挙動を防ぐことができます。
前提知識の解説
このコミットを理解するためには、以下のGo言語の基本的な概念と標準ライブラリの知識が必要です。
io.Reader
インターフェース: Go言語におけるデータの読み込み元を抽象化するインターフェースです。Read(p []byte) (n int, err error)
メソッドを持ち、バイトスライスp
にデータを読み込み、読み込んだバイト数n
とエラーerr
を返します。os.Stdin
やbytes.Buffer
、ファイルなどがこのインターフェースを実装しています。bufio
パッケージ: バッファリングされたI/O操作を提供するパッケージです。bufio.Reader
は、io.Reader
をラップしてバッファリングを行い、効率的な読み込みを可能にします。strings
パッケージ: 文字列操作に関するユーティリティ関数を提供するパッケージです。strings.Fields
は、文字列をスペースで分割して単語のリストを返します。unicode
パッケージ: Unicode文字に関するプロパティ(例えば、文字が数字か、空白文字かなど)を扱うための関数を提供するパッケージです。unicode.IsSpace
は、与えられたルーンがUnicodeの空白文字であるかどうかを判定します。- ルーン (Rune): Go言語では、Unicodeコードポイントを表現するために
rune
型(int32
のエイリアス)を使用します。文字列はバイトのシーケンスですが、UTF-8エンコーディングされたルーンのシーケンスとして扱うことができます。 - UTF-8: Unicode文字をバイト列にエンコードするための可変長エンコーディング方式です。Go言語の文字列は内部的にUTF-8でエンコードされています。
- エラーハンドリング: Go言語では、関数がエラーを返す場合、通常は戻り値の最後の要素として
error
型の値を返します。呼び出し元は、このエラー値をチェックして適切な処理を行います。io.EOF
は、入力の終端に達したことを示す特別なエラーです。
技術的詳細
bufio.Scanner
の設計思想
bufio.Scanner
は、ストリームから「トークン」を抽出するための高レベルなインターフェースを提供します。その核心は、以下の3つの主要な要素にあります。
-
Scanner
構造体:r io.Reader
: 入力元となるio.Reader
。split SplitFunc
: トークンを分割するための関数。maxTokenSize int
: 許容されるトークンの最大サイズ。これを超えるとErrTooLong
エラーが発生します。token []byte
: 直前にスキャンされたトークンのバイトスライス。buf []byte
: 内部バッファ。io.Reader
から読み込んだデータを一時的に保持します。start int
,end int
:buf
内の有効なデータの範囲を示すインデックス。err error
: スキャン中に発生した最初のエラーを保持します。
-
SplitFunc
型:type SplitFunc func(data []byte, atEOF bool) (advance int, token []byte, err error)
この関数は、Scanner
の心臓部であり、入力データdata
からトークンを識別し、そのトークンが消費したバイト数advance
、抽出されたトークンtoken
、および発生したエラーerr
を返します。data []byte
: 現在処理中の入力データのスライス。atEOF bool
: 入力ストリームがEOFに達しているかどうかを示すフラグ。advance int
:data
スライス内で、次のスキャンを開始するためにスキップすべきバイト数。token []byte
: 抽出されたトークン。err error
: 分割中に発生したエラー。
SplitFunc
が(0, nil, nil)
を返した場合、それは「現在利用可能なデータでは完全なトークンを形成できないため、さらにデータを読み込む必要がある」ことをScanner
に伝えます。 -
Scan()
メソッド:func (s *Scanner) Scan() bool
このメソッドがScanner
の主要な操作です。Scan()
が呼び出されるたびに、SplitFunc
を使用して次のトークンを抽出しようとします。- 内部バッファに十分なデータがない場合、
s.r.Read()
を呼び出してデータを補充します。 - バッファが最大トークンサイズを超えそうになった場合、バッファを拡張します。
SplitFunc
がトークンを返した場合、s.token
に設定し、true
を返します。- EOFに達するか、I/Oエラーまたは
SplitFunc
からのエラーが発生した場合、false
を返します。
- 内部バッファに十分なデータがない場合、
デフォルトのSplitFunc
の実装
このコミットでは、いくつかの便利なSplitFunc
が提供されています。
-
ScanLines
:- 行末の
\r?\n
(CRLFまたはLF)を削除して行を返します。 - 最後の行に改行がない場合でも、その行をトークンとして返します。
bytes.IndexByte(data, '\n')
を使用して改行文字を探します。dropCR
ヘルパー関数で末尾の\r
を削除します。
- 行末の
-
ScanBytes
:- 入力の各バイトを個別のトークンとして返します。
advance
は常に1
です。
-
ScanRunes
:- 入力の各UTF-8エンコードされたルーンをトークンとして返します。
- 不正なUTF-8シーケンスは
U+FFFD
(utf8.RuneError
)に変換されますが、advance
は1バイトのみ進みます。これは、Goの文字列に対するrange
ループの挙動と一致します。 utf8.DecodeRune
とutf8.FullRune
を使用してルーンのデコードと完全性のチェックを行います。
-
ScanWords
:- スペースで区切られた単語をトークンとして返します。
- 単語の定義には
isSpace
関数(後述)を使用し、unicode.IsSpace
と互換性があります。 - 先頭と末尾のスペースはスキップされます。
strings.Fields
の変更
strings.Fields
関数は、文字列を1つ以上の連続する空白文字で分割する関数です。このコミットでは、その空白文字の定義がunicode.IsSpace
によって定義されるように変更されました。
変更前: 内部的なASCIIベースの空白文字判定
変更後: unicode.IsSpace
に基づくUnicodeの空白文字判定
これにより、strings.Fields
とbufio.Scanner
のScanWords
が、より広範なUnicode空白文字(例えば、ノーブレークスペース\u00A0
や全角スペース\u3000
など)に対して一貫した挙動を示すようになります。
エラー処理
Scanner
は、以下のエラーを定義しています。
ErrTooLong
: トークンがmaxTokenSize
を超えた場合に発生します。ErrNegativeAdvance
:SplitFunc
が負のadvance
値を返した場合に発生します。ErrAdvanceTooFar
:SplitFunc
がdata
スライスの範囲を超えるadvance
値を返した場合に発生します。
Scan()
メソッドがfalse
を返した後、Err()
メソッドを呼び出すことで、スキャン中に発生したエラー(io.EOF
を除く)を取得できます。
コアとなるコードの変更箇所
このコミットで変更された主要なファイルは以下の通りです。
src/pkg/bufio/bufio_test.go
: 既存のbufio
テストファイル。TestNegativeRead
関数内のt.Fatal
がt.Fatalf
に変更されています。これは小さな修正ですが、テストの出力形式を改善するものです。src/pkg/bufio/export_test.go
: 新規追加されたファイル。テスト目的でbufio
パッケージの内部関数やフィールドをエクスポートします。IsSpace
(isSpace
のテスト用)、Scanner.MaxTokenSize
(テストでバッファサイズを制御するため)、Scanner.ErrOrEOF
(特定のテストケース用)が含まれます。src/pkg/bufio/scan.go
: このコミットの主要な変更点。Scanner
型、SplitFunc
型、関連するエラー変数、NewScanner
コンストラクタ、Scan
メソッド、Bytes
、Text
、Err
、Split
メソッド、そしてデフォルトのSplitFunc
実装(ScanBytes
,ScanRunes
,ScanLines
,ScanWords
)が定義されています。src/pkg/bufio/scan_test.go
: 新規追加されたテストファイル。bufio.Scanner
の新しい機能(ScanBytes
,ScanRunes
,ScanWords
,ScanLines
)を網羅的にテストします。特に、TestSpace
はisSpace
関数がunicode.IsSpace
と一致するかどうかを検証しています。また、長い行の処理、改行なしの行、エラー処理など、様々なエッジケースがテストされています。src/pkg/strings/strings.go
:Fields
関数のコメントが更新され、unicode.IsSpace
を使用することが明記されました。また、Fields
関数自体もFieldsFunc(s, unicode.IsSpace)
を呼び出すように変更され、unicode
パッケージへの依存が導入されました。
コアとなるコードの解説
src/pkg/bufio/scan.go
Scanner
構造体
type Scanner struct {
r io.Reader // The reader provided by the client.
split SplitFunc // The function to split the tokens.
maxTokenSize int // Maximum size of a token; modified by tests.
token []byte // Last token returned by split.
buf []byte // Buffer used as argument to split.
start int // First non-processed byte in buf.
end int // End of data in buf.
err error // Sticky error.
}
Scanner
の内部状態を管理する構造体です。r
は入力元、split
は分割ロジック、buf
は内部バッファ、token
は直前の結果を保持します。
Scan()
メソッド
func (s *Scanner) Scan() bool {
// Loop until we have a token.
for {
// See if we can get a token with what we already have.
if s.end > s.start {
advance, token, err := s.split(s.buf[s.start:s.end], s.err != nil)
if err != nil {
s.setErr(err)
return false
}
if !s.advance(advance) {
return false
}
s.token = token
if token != nil {
return true
}
}
// ... (buffer management and reading more data) ...
}
panic("not reached")
}
Scan()
メソッドは、Scanner
の主要なループを実装しています。
- まず、現在のバッファ内のデータでトークンを生成できるか
s.split
関数を呼び出して試みます。 s.split
がエラーを返した場合、そのエラーを記録し、false
を返してスキャンを停止します。s.split
がトークンを返した場合、s.token
に設定し、true
を返します。- トークンが生成できない場合(
s.split
が(0, nil, nil)
を返した場合)、またはバッファが空の場合、io.Reader
からさらにデータを読み込み、バッファを拡張するなどの処理を行います。
SplitFunc
の例: ScanLines
func ScanLines(data []byte, atEOF bool) (advance int, token []byte, err error) {
if atEOF && len(data) == 0 {
return 0, nil, nil
}
if i := bytes.IndexByte(data, '\n'); i >= 0 {
// We have a full newline-terminated line.
return i + 1, dropCR(data[0:i]), nil
}
// If we're at EOF, we have a final, non-terminated line. Return it.
if atEOF {
return len(data), dropCR(data), nil
}
// Request more data.
return 0, nil, nil
}
ScanLines
は、行単位でデータを分割するSplitFunc
の典型的な実装です。
data
中に改行文字\n
が見つかった場合、その位置までをトークンとし、改行文字を含めてadvance
します。dropCR
で末尾の\r
を削除します。atEOF
がtrue
で、かつdata
が空でない場合(つまり、入力の終端に達し、かつ最後の行に改行がない場合)、残りのdata
全体をトークンとして返します。- それ以外の場合(改行が見つからず、EOFでもない場合)、
(0, nil, nil)
を返して、Scanner
にさらにデータを読み込むように要求します。
isSpace
関数
func isSpace(r rune) bool {
if r <= '\u00FF' {
// Obvious ASCII ones: \t through \r plus space. Plus two Latin-1 oddballs.
switch r {
case ' ', '\t', '\n', '\v', '\f', '\r':
return true
case '\u0085', '\u00A0':
return true
}
return false
}
// High-valued ones.
if '\u2000' <= r && r <= '\u200a' {
return true
}
switch r {
case '\u1680', '\u180e', '\u2028', '\u2029', '\u202f', '\u205f', '\u3000':
return true
}
return false
}
isSpace
は、与えられたルーンが空白文字であるかを判定する内部関数です。unicode
パッケージに依存しないように、手動で一般的なASCIIおよび一部のUnicode空白文字をチェックしています。scan_test.go
のTestSpace
で、この関数がunicode.IsSpace
と一致するかどうかが検証されています。
src/pkg/strings/strings.go
Fields
関数の変更
func Fields(s string) []string {
return FieldsFunc(s, unicode.IsSpace)
}
strings.Fields
関数は、以前は内部的なロジックで空白文字を判定していましたが、このコミットでunicode.IsSpace
を引数としてFieldsFunc
を呼び出すように変更されました。これにより、strings.Fields
の空白文字の定義がUnicodeの標準に準拠し、bufio.Scanner
のScanWords
と一貫性を持つようになりました。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/55ad7b9bfe86c90cff55e0e8926fd8ff6b3b5182
- Go Issue #4802: https://code.google.com/p/go/issues/detail?id=4802 (現在はGitHubに移行済み: https://github.com/golang/go/issues/4802)
- Go CL 7322088: https://golang.org/cl/7322088
参考にした情報源リンク
- Go言語の公式ドキュメント (
bufio
パッケージ): https://pkg.go.dev/bufio - Go言語の公式ドキュメント (
strings
パッケージ): https://pkg.go.dev/strings - Go言語の公式ドキュメント (
unicode
パッケージ): https://pkg.go.dev/unicode - Go言語の公式ドキュメント (
io
パッケージ): https://pkg.go.dev/io - Go言語の公式ブログ: https://go.dev/blog/ (特定の記事は直接参照していませんが、Goの設計思想を理解する上で役立ちます)
- Stack Overflowなどのコミュニティサイト (一般的なGoのテキスト処理や
bufio
に関する議論) - Go言語のソースコード (特に
src/pkg/bufio/scan.go
とsrc/pkg/strings/strings.go
の変更履歴)