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

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

このコミットは、Go言語のfmtパッケージにおけるFscanf関数の挙動を修正するものです。具体的には、Fscanfが改行文字(\n)を読み飛ばさずに、行単位でのインタラクティブな入力処理が正しく機能するように改善されています。これにより、Issue 3481で報告された「Scanf/Fscanfが1文字多く読み込み、バッファリングされていないReaderでその文字が失われる」という問題が解決されます。

コミット

commit 2a0fdf6ea05dc31526e95990aadc2b327933cce1
Author: Rob Pike <r@golang.org>
Date:   Mon Jun 11 17:52:09 2012 -0400

    fmt.Fscanf: don't read past newline
    Makes interactive uses work line-by-line.
    Fixes #3481.
    
    R=golang-dev, bradfitz, r
    CC=golang-dev
    https://golang.org/cl/6297075
---\n src/pkg/fmt/scan.go      |  3 ++-\n src/pkg/fmt/scan_test.go | 27 +++++++++++++++++++++++++++\n 2 files changed, 29 insertions(+), 1 deletion(-)\n

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

https://github.com/golang/go/commit/2a0fdf6ea05dc31526e95990aadc2b327933cce1

元コミット内容

このコミットの目的は、fmt.Fscanf関数が改行文字を越えて読み進まないようにすることです。これにより、インタラクティブなアプリケーションが行単位で正しく動作するようになります。また、GoのIssue 3481を修正します。

変更の背景

この変更は、Go言語のfmtパッケージにおけるFscanf関数の既存のバグ、具体的にはIssue 3481「Scanf/Fscanf reads one rune to much, causing it to be lost in an unbuffered Reader」を修正するために行われました。

従来のFscanfの実装では、入力ストリームからデータを読み取る際に、フォーマット文字列にスペースが含まれている場合、入力中のスペース(またはEOF)を読み飛ばす挙動がありました。この「読み飛ばし」のロジックが、特に改行文字の扱いで問題を引き起こしていました。

問題の核心は、Fscanfが改行文字を単なる空白文字として扱い、次の非空白文字まで読み進もうとすることにありました。しかし、インタラクティブな入力、特に標準入力(os.Stdin)のようなバッファリングされていないリーダーを使用する場合、ユーザーがEnterキーを押して改行文字を入力すると、その行の入力は終了したと見なされるべきです。Fscanfが改行文字を読み飛ばしてさらに次の行の先頭まで読み進んでしまうと、その改行文字が消費され、後続のScanfFscanfの呼び出しで予期せぬ挙動(例えば、次の行の最初の文字が失われる、または次のScanfが空の入力を受け取ってしまう)が発生する可能性がありました。

この挙動は、特にコマンドラインインターフェース(CLI)アプリケーションなどで、ユーザーからの行単位の入力を処理する際に問題となります。ユーザーが1行入力し、Enterを押して次の入力を待っているにもかかわらず、プログラムがすでに次の行の先頭を読み込んでしまっていると、直感に反する動作となり、開発者が意図したインタラクティブなフローを構築することが困難になります。

このコミットは、この問題を解決し、Fscanfが改行文字に遭遇した時点で読み取りを停止することで、より予測可能でインタラクティブな入力処理を可能にすることを目的としています。

前提知識の解説

fmtパッケージとスキャン関数

Go言語のfmtパッケージは、フォーマットされたI/O(入出力)を実装するための機能を提供します。これには、Printfのような出力関数だけでなく、ScanfFscanfSscanfのような入力(スキャン)関数も含まれます。

  • fmt.Scanf: 標準入力(os.Stdin)からフォーマットされた文字列を読み取ります。
  • fmt.Fscanf: 指定されたio.Readerインターフェースからフォーマットされた文字列を読み取ります。
  • fmt.Sscanf: 文字列からフォーマットされた文字列を読み取ります。

これらのスキャン関数は、C言語のscanfファミリーと同様に、フォーマット文字列に基づいて入力ストリームからデータを解析し、指定された変数に格納します。フォーマット文字列には、%d(整数)、%s(文字列)、%v(デフォルトフォーマット)などの動詞が含まれます。

io.Readerインターフェース

io.Readerは、Go言語における基本的な入力インターフェースです。これは単一のReadメソッドを定義しています。

type Reader interface {
    Read(p []byte) (n int, err error)
}

Readメソッドは、データをpスライスに読み込み、読み込んだバイト数nとエラーerrを返します。Fscanfは、このio.Readerインターフェースを実装する任意の型(例: os.Stdinbytes.Bufferstrings.Readerなど)から入力を受け取ることができます。

Scanfにおける空白文字の扱い

Scanf系の関数は、デフォルトでフォーマット文字列中の空白文字(スペース、タブ、改行など)を、入力中の任意の数の空白文字とマッチさせ、それらを読み飛ばす挙動を持っています。例えば、フォーマット文字列が"%d %d"である場合、入力が"123 456"であっても、"123\n456"であっても、両方の整数を正しく読み取ることができます。

しかし、この「読み飛ばし」の挙動が、特に改行文字の扱いで問題となる場合があります。インタラクティブな入力では、ユーザーがEnterキーを押すことで行の終わりを示すため、改行文字は単なる空白文字以上の意味を持つことがあります。つまり、改行文字に遭遇したら、それ以上は読み進まずに、その行の処理を完了すべきという期待があるのです。

runeEOF

  • rune: Go言語では、Unicodeコードポイントを表すためにrune型が使用されます。これは実質的にint32のエイリアスです。入力ストリームから文字を読み取る際、バイト列をruneにデコードして処理することが一般的です。
  • EOF (End Of File): 入力ストリームの終端を示す特別な値です。io.Readerがこれ以上読み取るデータがない場合に返されるエラー(io.EOF)に対応します。fmtパッケージの内部では、EOFを表現するために特定のrune値(通常は負の値)が使用されることがあります。

このコミットは、Fscanfの内部ロジック、特にadvance関数が入力ストリームからruneを読み取る際に、EOFだけでなく改行文字も特別な終了条件として扱うように変更することで、上記の問題を解決しています。

技術的詳細

このコミットの技術的な核心は、fmtパッケージ内のスキャン処理を担当するscan.goファイルの(*ss) advanceメソッドの変更にあります。このメソッドは、フォーマット文字列中の空白文字に対応する入力中の空白文字を読み飛ばす役割を担っています。

変更前のコードでは、advanceメソッドが入力からruneを読み取る際に、eof(ファイルの終端)に達した場合にのみ読み取りを停止していました。

// 変更前
inputc := s.getRune()
if inputc == eof {
    return
}
if !isSpace(inputc) {
    // ...
}

このロジックでは、フォーマット文字列にスペースがある場合、Fscanfは入力中のスペースを消費し続けます。これには改行文字も含まれるため、ユーザーがEnterキーを押して改行文字を入力しても、Fscanfはそれを読み飛ばして次の行の先頭まで読み進んでしまう可能性がありました。これが、インタラクティブな使用において問題を引き起こしていました。

このコミットでは、このif条件に|| inputc == '\n'が追加されました。

// 変更後
inputc := s.getRune()
if inputc == eof || inputc == '\n' {
    // If we've reached a newline, stop now; don't read ahead.
    return
}
if !isSpace(inputc) {
    // ...
}

この変更により、advanceメソッドは、入力から読み取ったruneeofであるか、または改行文字(\n)である場合に、直ちに処理を終了し、それ以上読み進まなくなります。コメント「// If we've reached a newline, stop now; don't read ahead.」が示すように、これは改行文字に遭遇した時点で「読み進むのをやめる」という明確な意図を持っています。

この修正によって、Fscanfはフォーマット文字列にスペースが含まれていても、改行文字を越えて次の行のデータを先読みすることがなくなります。これにより、特にバッファリングされていないリーダー(例: os.Stdin)を使用する際に、行単位の入力処理がより予測可能になり、ユーザーがEnterキーを押した時点でその行の入力が完了したと見なされるようになります。

また、この変更を検証するために、scan_test.goTestLineByLineFscanfという新しいテストケースが追加されました。このテストは、simpleReaderというstrings.Readerをラップしたカスタムリーダーを使用しています。simpleReaderReadメソッドのみを実装し、ReadRuneは実装していません。これは、Fscanfが内部的にReadRuneを使用する際に、バッファリングされていないリーダーの挙動をシミュレートし、先読みの問題が実際に発生するかどうかを検証するのに役立ちます。

テストケースでは、"1\n2\n"という文字列をsimpleReaderに与え、Fscanfを2回呼び出して、それぞれ"1""2"が正しく読み取られ、改行文字が適切に処理されることを確認しています。これにより、修正が意図通りに機能していることが保証されます。

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

src/pkg/fmt/scan.go の変更点:

--- a/src/pkg/fmt/scan.go
+++ b/src/pkg/fmt/scan.go
@@ -1090,7 +1090,8 @@ func (s *ss) advance(format string) (i int) {
 			// There was space in the format, so there should be space (EOF)
 			// in the input.
 			inputc := s.getRune()
-			if inputc == eof {
+			if inputc == eof || inputc == '\n' {
+				// If we've reached a newline, stop now; don't read ahead.
 				return
 			}
 			if !isSpace(inputc) {

src/pkg/fmt/scan_test.go の変更点 (追加されたテスト):

--- a/src/pkg/fmt/scan_test.go
+++ b/src/pkg/fmt/scan_test.go
@@ -810,6 +810,33 @@ func TestMultiLine(t *testing.T) {
 	}\n}\n \n+// simpleReader is a strings.Reader that implements only Read, not ReadRune.\n+// Good for testing readahead.\n+type simpleReader struct {\n+\tsr *strings.Reader\n+}\n+\n+func (s *simpleReader) Read(b []byte) (n int, err error) {\n+\treturn s.sr.Read(b)\n+}\n+\n+// Test that Fscanf does not read past newline. Issue 3481.\n+func TestLineByLineFscanf(t *testing.T) {\n+\tr := &simpleReader{strings.NewReader("1\\n2\\n")}\n+\tvar i, j int\n+\tn, err := Fscanf(r, "%v\\n", &i)\n+\tif n != 1 || err != nil {\n+\t\tt.Fatalf("first read: %d %q", n, err)\n+\t}\n+\tn, err = Fscanf(r, "%v\\n", &j)\n+\tif n != 1 || err != nil {\n+\t\tt.Fatalf("second read: %d %q", n, err)\n+\t}\n+\tif i != 1 || j != 2 {\n+\t\tt.Errorf("wrong values; wanted 1 2 got %d %d", i, j)\n+\t}\n+}\n+\n // RecursiveInt accepts a string matching %d.%d.%d....\n // and parses it into a linked list.\n // It allows us to benchmark recursive descent style scanners.\n```

## コアとなるコードの解説

### `src/pkg/fmt/scan.go` の変更

この変更は、`fmt`パッケージ内部の`(*ss) advance`メソッドにあります。このメソッドは、`Fscanf`のようなスキャン関数が入力ストリームから文字を読み進める際のロジックを制御しています。特に、フォーマット文字列に空白文字(例: `%d %d`の間のスペース)が含まれている場合に、入力中の対応する空白文字を読み飛ばす役割を担っています。

変更前のコードでは、`inputc := s.getRune()`で入力から1文字(rune)を読み取った後、`if inputc == eof`という条件でファイルの終端(EOF)に達したかどうかのみをチェックしていました。EOFであれば、それ以上読み進まずにメソッドを終了します。

このコミットでは、この条件が`if inputc == eof || inputc == '\n'`に変更されました。
これは、読み取った文字`inputc`がEOFであるか、**または改行文字(`\n`)である場合**に、直ちに`advance`メソッドを終了するという意味です。

この修正の意図は、追加されたコメント「`// If we've reached a newline, stop now; don't read ahead.`」に明確に示されています。つまり、`Fscanf`が入力ストリームから改行文字を読み取った場合、たとえフォーマット文字列にまだ読み飛ばすべき空白文字の指示があったとしても、それ以上先読みせずに、その時点で読み取りを停止するということです。

これにより、以下のような効果が生まれます。

1.  **行単位のインタラクティブ入力の改善**: ユーザーがEnterキーを押して改行文字を入力した場合、`Fscanf`はその改行文字を消費し、それ以上次の行のデータを先読みしなくなります。これにより、次の`Scanf`呼び出しが新しい行の先頭から始まることが保証され、ユーザーの入力とプログラムの読み取りが同期しやすくなります。
2.  **失われる文字の防止**: バッファリングされていないリーダー(例: `os.Stdin`)を使用している場合、改行文字を越えて先読みしてしまうと、その先読みされた文字が内部バッファに残り、次の`Scanf`呼び出しで予期せず消費されてしまう、あるいは失われてしまう可能性がありました。この修正により、そのような問題が回避されます。

### `src/pkg/fmt/scan_test.go` の変更

このコミットでは、上記の修正が正しく機能することを検証するために、`TestLineByLineFscanf`という新しいテスト関数が追加されました。

このテストの重要な部分は、`simpleReader`というカスタムの`io.Reader`実装を使用している点です。

```go
type simpleReader struct {
	sr *strings.Reader
}

func (s *simpleReader) Read(b []byte) (n int, err error) {
	return s.sr.Read(b)
}

simpleReaderは、内部に*strings.Readerを持ち、そのReadメソッドを呼び出すだけです。しかし、strings.Readerが実装しているReadRuneメソッドは実装していません。Fscanfは内部的にio.Readerio.ByteScannerio.RuneScannerインターフェースを実装している場合に、より効率的なReadRuneメソッドを使用しようとします。simpleReaderのようにReadRuneを実装しないリーダーを使用することで、FscanfReadメソッドのみを使用して文字を読み取る場合の挙動、特に先読みの問題が発生しやすいシナリオをシミュレートできます。

テストケースでは、"1\n2\n"という文字列をsimpleReaderに与え、Fscanfを2回呼び出しています。

r := &simpleReader{strings.NewReader("1\n2\n")}
var i, j int
n, err := Fscanf(r, "%v\n", &i) // 最初の読み取り
// ...
n, err = Fscanf(r, "%v\n", &j) // 2番目の読み取り
// ...
if i != 1 || j != 2 {
    t.Errorf("wrong values; wanted 1 2 got %d %d", i, j)
}

最初のFscanf(r, "%v\n", &i)は、"1"とそれに続く改行文字を読み取ります。修正がなければ、このFscanf"1\n"を読み取った後、さらに次の行の"2"の先頭まで読み進んでしまう可能性がありました。しかし、修正後は改行文字に遭遇した時点で読み取りを停止するため、"1\n"までが消費され、リーダーの次の読み取り位置は"2"の先頭になります。

2番目のFscanf(r, "%v\n", &j)は、残りの入力から"2"とそれに続く改行文字を読み取ります。もし最初のFscanf"2"まで先読みしてしまっていたら、2番目のFscanf"2"を正しく読み取ることができなかったでしょう。

このテストは、i1j2と正しく読み取られることを検証することで、Fscanfが改行文字を越えて先読みしないという新しい挙動が期待通りに機能していることを保証しています。

関連リンク

参考にした情報源リンク