[インデックス 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
が改行文字を読み飛ばしてさらに次の行の先頭まで読み進んでしまうと、その改行文字が消費され、後続のScanf
やFscanf
の呼び出しで予期せぬ挙動(例えば、次の行の最初の文字が失われる、または次のScanf
が空の入力を受け取ってしまう)が発生する可能性がありました。
この挙動は、特にコマンドラインインターフェース(CLI)アプリケーションなどで、ユーザーからの行単位の入力を処理する際に問題となります。ユーザーが1行入力し、Enterを押して次の入力を待っているにもかかわらず、プログラムがすでに次の行の先頭を読み込んでしまっていると、直感に反する動作となり、開発者が意図したインタラクティブなフローを構築することが困難になります。
このコミットは、この問題を解決し、Fscanf
が改行文字に遭遇した時点で読み取りを停止することで、より予測可能でインタラクティブな入力処理を可能にすることを目的としています。
前提知識の解説
fmt
パッケージとスキャン関数
Go言語のfmt
パッケージは、フォーマットされたI/O(入出力)を実装するための機能を提供します。これには、Printf
のような出力関数だけでなく、Scanf
、Fscanf
、Sscanf
のような入力(スキャン)関数も含まれます。
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.Stdin
、bytes.Buffer
、strings.Reader
など)から入力を受け取ることができます。
Scanf
における空白文字の扱い
Scanf
系の関数は、デフォルトでフォーマット文字列中の空白文字(スペース、タブ、改行など)を、入力中の任意の数の空白文字とマッチさせ、それらを読み飛ばす挙動を持っています。例えば、フォーマット文字列が"%d %d"
である場合、入力が"123 456"
であっても、"123\n456"
であっても、両方の整数を正しく読み取ることができます。
しかし、この「読み飛ばし」の挙動が、特に改行文字の扱いで問題となる場合があります。インタラクティブな入力では、ユーザーがEnterキーを押すことで行の終わりを示すため、改行文字は単なる空白文字以上の意味を持つことがあります。つまり、改行文字に遭遇したら、それ以上は読み進まずに、その行の処理を完了すべきという期待があるのです。
rune
とEOF
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
メソッドは、入力から読み取ったrune
がeof
であるか、または改行文字(\n
)である場合に、直ちに処理を終了し、それ以上読み進まなくなります。コメント「// If we've reached a newline, stop now; don't read ahead.
」が示すように、これは改行文字に遭遇した時点で「読み進むのをやめる」という明確な意図を持っています。
この修正によって、Fscanf
はフォーマット文字列にスペースが含まれていても、改行文字を越えて次の行のデータを先読みすることがなくなります。これにより、特にバッファリングされていないリーダー(例: os.Stdin
)を使用する際に、行単位の入力処理がより予測可能になり、ユーザーがEnterキーを押した時点でその行の入力が完了したと見なされるようになります。
また、この変更を検証するために、scan_test.go
にTestLineByLineFscanf
という新しいテストケースが追加されました。このテストは、simpleReader
というstrings.Reader
をラップしたカスタムリーダーを使用しています。simpleReader
はRead
メソッドのみを実装し、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.Reader
がio.ByteScanner
やio.RuneScanner
インターフェースを実装している場合に、より効率的なReadRune
メソッドを使用しようとします。simpleReader
のようにReadRune
を実装しないリーダーを使用することで、Fscanf
がRead
メソッドのみを使用して文字を読み取る場合の挙動、特に先読みの問題が発生しやすいシナリオをシミュレートできます。
テストケースでは、"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"
を正しく読み取ることができなかったでしょう。
このテストは、i
が1
、j
が2
と正しく読み取られることを検証することで、Fscanf
が改行文字を越えて先読みしないという新しい挙動が期待通りに機能していることを保証しています。
関連リンク
- Go Issue 3481: https://github.com/golang/go/issues/3481
- Go CL 6297075: https://golang.org/cl/6297075 (Goのコードレビューシステムにおける変更リスト)
参考にした情報源リンク
- Go Issue 3481: Scanf/Fscanf reads one rune to much, causing it to be lost in an unbuffered Reader: https://github.com/golang/go/issues/3481
- Go
fmt
package documentation: https://pkg.go.dev/fmt - Go
io
package documentation: https://pkg.go.dev/io - Go
strings
package documentation: https://pkg.go.dev/strings