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

[インデックス 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: ReadStringReadLineの代替として、より良い行スキャナーが必要」という内容で、bufio.ReaderReadLineメソッドが返すバイトスライスが次の読み込みで上書きされる可能性があり、安全でないという問題提起がされていました。また、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.Stdinbytes.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つの主要な要素にあります。

  1. Scanner構造体:

    • r io.Reader: 入力元となるio.Reader
    • split SplitFunc: トークンを分割するための関数。
    • maxTokenSize int: 許容されるトークンの最大サイズ。これを超えるとErrTooLongエラーが発生します。
    • token []byte: 直前にスキャンされたトークンのバイトスライス。
    • buf []byte: 内部バッファ。io.Readerから読み込んだデータを一時的に保持します。
    • start int, end int: buf内の有効なデータの範囲を示すインデックス。
    • err error: スキャン中に発生した最初のエラーを保持します。
  2. 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に伝えます。

  3. 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+FFFDutf8.RuneError)に変換されますが、advanceは1バイトのみ進みます。これは、Goの文字列に対するrangeループの挙動と一致します。
    • utf8.DecodeRuneutf8.FullRuneを使用してルーンのデコードと完全性のチェックを行います。
  • ScanWords:

    • スペースで区切られた単語をトークンとして返します。
    • 単語の定義にはisSpace関数(後述)を使用し、unicode.IsSpaceと互換性があります。
    • 先頭と末尾のスペースはスキップされます。

strings.Fieldsの変更

strings.Fields関数は、文字列を1つ以上の連続する空白文字で分割する関数です。このコミットでは、その空白文字の定義がunicode.IsSpaceによって定義されるように変更されました。

変更前: 内部的なASCIIベースの空白文字判定 変更後: unicode.IsSpaceに基づくUnicodeの空白文字判定

これにより、strings.Fieldsbufio.ScannerScanWordsが、より広範なUnicode空白文字(例えば、ノーブレークスペース\u00A0や全角スペース\u3000など)に対して一貫した挙動を示すようになります。

エラー処理

Scannerは、以下のエラーを定義しています。

  • ErrTooLong: トークンがmaxTokenSizeを超えた場合に発生します。
  • ErrNegativeAdvance: SplitFuncが負のadvance値を返した場合に発生します。
  • ErrAdvanceTooFar: SplitFuncdataスライスの範囲を超えるadvance値を返した場合に発生します。

Scan()メソッドがfalseを返した後、Err()メソッドを呼び出すことで、スキャン中に発生したエラー(io.EOFを除く)を取得できます。

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

このコミットで変更された主要なファイルは以下の通りです。

  • src/pkg/bufio/bufio_test.go: 既存のbufioテストファイル。TestNegativeRead関数内のt.Fatalt.Fatalfに変更されています。これは小さな修正ですが、テストの出力形式を改善するものです。
  • src/pkg/bufio/export_test.go: 新規追加されたファイル。テスト目的でbufioパッケージの内部関数やフィールドをエクスポートします。IsSpaceisSpaceのテスト用)、Scanner.MaxTokenSize(テストでバッファサイズを制御するため)、Scanner.ErrOrEOF(特定のテストケース用)が含まれます。
  • src/pkg/bufio/scan.go: このコミットの主要な変更点。 Scanner型、SplitFunc型、関連するエラー変数、NewScannerコンストラクタ、Scanメソッド、BytesTextErrSplitメソッド、そしてデフォルトのSplitFunc実装(ScanBytes, ScanRunes, ScanLines, ScanWords)が定義されています。
  • src/pkg/bufio/scan_test.go: 新規追加されたテストファイル。 bufio.Scannerの新しい機能(ScanBytes, ScanRunes, ScanWords, ScanLines)を網羅的にテストします。特に、TestSpaceisSpace関数が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の主要なループを実装しています。

  1. まず、現在のバッファ内のデータでトークンを生成できるかs.split関数を呼び出して試みます。
  2. s.splitがエラーを返した場合、そのエラーを記録し、falseを返してスキャンを停止します。
  3. s.splitがトークンを返した場合、s.tokenに設定し、trueを返します。
  4. トークンが生成できない場合(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を削除します。
  • atEOFtrueで、かつ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.goTestSpaceで、この関数が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.ScannerScanWordsと一貫性を持つようになりました。

関連リンク

参考にした情報源リンク

  • 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.gosrc/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: ReadStringReadLineの代替として、より良い行スキャナーが必要」という内容で、bufio.ReaderReadLineメソッドが返すバイトスライスが次の読み込みで上書きされる可能性があり、安全でないという問題提起がされていました。また、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.Stdinbytes.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つの主要な要素にあります。

  1. Scanner構造体:

    • r io.Reader: 入力元となるio.Reader
    • split SplitFunc: トークンを分割するための関数。
    • maxTokenSize int: 許容されるトークンの最大サイズ。これを超えるとErrTooLongエラーが発生します。
    • token []byte: 直前にスキャンされたトークンのバイトスライス。
    • buf []byte: 内部バッファ。io.Readerから読み込んだデータを一時的に保持します。
    • start int, end int: buf内の有効なデータの範囲を示すインデックス。
    • err error: スキャン中に発生した最初のエラーを保持します。
  2. 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に伝えます。

  3. 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+FFFDutf8.RuneError)に変換されますが、advanceは1バイトのみ進みます。これは、Goの文字列に対するrangeループの挙動と一致します。
    • utf8.DecodeRuneutf8.FullRuneを使用してルーンのデコードと完全性のチェックを行います。
  • ScanWords:

    • スペースで区切られた単語をトークンとして返します。
    • 単語の定義にはisSpace関数(後述)を使用し、unicode.IsSpaceと互換性があります。
    • 先頭と末尾のスペースはスキップされます。

strings.Fieldsの変更

strings.Fields関数は、文字列を1つ以上の連続する空白文字で分割する関数です。このコミットでは、その空白文字の定義がunicode.IsSpaceによって定義されるように変更されました。

変更前: 内部的なASCIIベースの空白文字判定 変更後: unicode.IsSpaceに基づくUnicodeの空白文字判定

これにより、strings.Fieldsbufio.ScannerScanWordsが、より広範なUnicode空白文字(例えば、ノーブレークスペース\u00A0や全角スペース\u3000など)に対して一貫した挙動を示すようになります。

エラー処理

Scannerは、以下のエラーを定義しています。

  • ErrTooLong: トークンがmaxTokenSizeを超えた場合に発生します。
  • ErrNegativeAdvance: SplitFuncが負のadvance値を返した場合に発生します。
  • ErrAdvanceTooFar: SplitFuncdataスライスの範囲を超えるadvance値を返した場合に発生します。

Scan()メソッドがfalseを返した後、Err()メソッドを呼び出すことで、スキャン中に発生したエラー(io.EOFを除く)を取得できます。

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

このコミットで変更された主要なファイルは以下の通りです。

  • src/pkg/bufio/bufio_test.go: 既存のbufioテストファイル。TestNegativeRead関数内のt.Fatalt.Fatalfに変更されています。これは小さな修正ですが、テストの出力形式を改善するものです。
  • src/pkg/bufio/export_test.go: 新規追加されたファイル。テスト目的でbufioパッケージの内部関数やフィールドをエクスポートします。IsSpaceisSpaceのテスト用)、Scanner.MaxTokenSize(テストでバッファサイズを制御するため)、Scanner.ErrOrEOF(特定のテストケース用)が含まれます。
  • src/pkg/bufio/scan.go: このコミットの主要な変更点。 Scanner型、SplitFunc型、関連するエラー変数、NewScannerコンストラクタ、Scanメソッド、BytesTextErrSplitメソッド、そしてデフォルトのSplitFunc実装(ScanBytes, ScanRunes, ScanLines, ScanWords)が定義されています。
  • src/pkg/bufio/scan_test.go: 新規追加されたテストファイル。 bufio.Scannerの新しい機能(ScanBytes, ScanRunes, ScanWords, ScanLines)を網羅的にテストします。特に、TestSpaceisSpace関数が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の主要なループを実装しています。

  1. まず、現在のバッファ内のデータでトークンを生成できるかs.split関数を呼び出して試みます。
  2. s.splitがエラーを返した場合、そのエラーを記録し、falseを返してスキャンを停止します。
  3. s.splitがトークンを返した場合、s.tokenに設定し、trueを返します。
  4. トークンが生成できない場合(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を削除します。
  • atEOFtrueで、かつ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.goTestSpaceで、この関数が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.ScannerScanWordsと一貫性を持つようになりました。

関連リンク

参考にした情報源リンク

  • 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.gosrc/pkg/strings/strings.goの変更履歴)