[インデックス 19229] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージの改善に関するものです。bufio
パッケージは、I/O操作をバッファリングすることで効率化するための機能を提供します。具体的には、bufio.Reader
は入力ストリームからの読み込みをバッファリングし、bufio.Writer
は出力ストリームへの書き込みをバッファリングします。
このコミットの主な目的は、bufio.Reader
の様々な読み込み関数(ReadBytes
, ReadSlice
, ReadString
など)が、UnreadByte
メソッドと組み合わせて使用された際に、常に期待通りに動作するように修正することです。
コミット
bufio: make all read functions UnreadByte-friendly
Fixes #7844.
LGTM=crawshaw
R=golang-codereviews, crawshaw
CC=golang-codereviews
https://golang.org/cl/90620045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9144d8752a4a1cdee1a9c7ebd2c5ae2739293509
元コミット内容
bufio: make all read functions UnreadByte-friendly
このコミットは、bufio
パッケージ内のすべての読み込み関数がUnreadByte
メソッドと互換性を持つように修正します。これは、GoのIssue #7844で報告された問題を解決するためのものです。
変更の背景
Go言語のbufio
パッケージは、効率的なI/O操作のためにバッファリング機能を提供します。bufio.Reader
には、ReadByte
で読み込んだ最後のバイトをバッファに戻すためのUnreadByte
メソッドが存在します。しかし、ReadSlice
やReadBytes
、ReadString
といった、複数のバイトを一度に読み込む関数を使用した後にUnreadByte
を呼び出すと、期待通りの動作をしないという問題が報告されていました(Issue #7844)。
この問題は、UnreadByte
が正しく機能するために必要な「最後に読み込んだバイト」の情報が、これらの関数によって適切に更新されていなかったことに起因します。UnreadByte
は、bufio.Reader
内部のlastByte
フィールドに格納された値を利用して、バッファにバイトを戻します。しかし、ReadSlice
などの関数が、読み込み処理の途中でエラーが発生したり、特定の条件で早期に終了したりした場合に、lastByte
が正しく設定されないことがありました。
この不整合は、bufio.Reader
のAPI契約の一部であるUnreadByte
の信頼性を損ねるものであり、ユーザーが予期しない動作に遭遇する可能性がありました。そのため、すべての読み込み関数がUnreadByte
の期待する動作をサポートするように修正する必要がありました。
前提知識の解説
bufio
パッケージ
bufio
パッケージは、Go言語におけるバッファリングされたI/Oを提供する標準ライブラリです。これにより、ディスクI/OやネットワークI/Oなどの低レベルな読み書き操作の回数を減らし、アプリケーションのパフォーマンスを向上させることができます。
bufio.Reader
: 入力ストリームからの読み込みをバッファリングします。これにより、一度に大量のデータを読み込み、アプリケーションが必要とする分だけをバッファから提供することで、システムコールを削減します。bufio.Writer
: 出力ストリームへの書き込みをバッファリングします。同様に、書き込み操作をまとめて実行することで、効率を高めます。
UnreadByte
メソッド
bufio.Reader
のUnreadByte
メソッドは、直前にReadByte
、ReadRune
、またはその他の読み込み関数によって読み込まれた最後のバイトを、内部バッファの先頭に戻す機能を提供します。これにより、プログラムは読み込んだバイトを「取り消し」て、再度読み込み直すことができます。これは、例えばパーサーが特定の区切り文字を読み込んだ後、その区切り文字を次の処理のためにバッファに戻したい場合などに有用です。
UnreadByte
が正しく機能するためには、bufio.Reader
が最後に読み込んだバイトを正確に記憶している必要があります。この情報は、bufio.Reader
構造体の内部フィールドであるlastByte
に格納されます。
関連する読み込み関数
ReadByte()
: 1バイトを読み込みます。ReadRune()
: 1つのUnicodeルーン(文字)を読み込みます。ReadSlice(delim byte)
: 指定された区切り文字(delim
)が見つかるまでバイトを読み込み、その区切り文字を含むバイトスライスを返します。ReadBytes(delim byte)
:ReadSlice
と同様に区切り文字まで読み込みますが、バイトスライスをコピーして返します。ReadString(delim byte)
:ReadBytes
と同様に区切り文字まで読み込みますが、結果を文字列として返します。ReadLine()
: 改行文字(\n
または\r\n
)までを読み込み、その行のバイトスライスを返します。改行文字自体は返されるスライスには含まれません。
lastByte
フィールド
bufio.Reader
構造体には、lastByte
という内部フィールドがあります。このフィールドは、UnreadByte
メソッドが機能するために非常に重要です。ReadByte
やReadRune
などの読み込み関数がバイトを読み込むたびに、その読み込んだバイトがlastByte
に格納されます。UnreadByte
が呼び出されると、このlastByte
に格納されたバイトがバッファに戻されます。
問題は、ReadSlice
やReadBytes
、ReadString
といった複数のバイトを読み込む関数が、読み込みの途中でlastByte
を適切に更新していなかったり、更新のタイミングが不適切だったりしたことです。これにより、これらの関数が終了した後にUnreadByte
を呼び出すと、期待とは異なるバイトが戻されるか、エラーが発生する可能性がありました。
技術的詳細
このコミットの技術的な核心は、bufio.Reader
のReadSlice
関数におけるlastByte
フィールドの更新ロジックの修正にあります。
UnreadByte
が正しく機能するための条件
UnreadByte
メソッドは、直前の読み込み操作によってバッファから取り出された最後のバイトを正確に「記憶」している必要があります。この「記憶」は、bufio.Reader
の内部状態であるlastByte
フィールドによって実現されます。したがって、すべての読み込み関数は、読み込みが完了した時点で、その操作によって消費された最後のバイトをlastByte
に設定する責任があります。
変更前の問題点
変更前のReadSlice
関数は、複数のリターンパスを持っていました。例えば、区切り文字が見つかった場合、バッファが一杯になった場合、または読み込み中にエラーが発生した場合などです。これらの異なるリターンパスにおいて、lastByte
が常に適切に更新されるとは限りませんでした。
特に、ReadSlice
が区切り文字を見つけてbreak
する際や、エラーが発生してbreak
する際に、line
変数に格納されたバイト列の最後のバイトがlastByte
に設定される前に関数が終了してしまう可能性がありました。これにより、UnreadByte
が呼び出されたときに、古いlastByte
の値が使用されるか、あるいはlastByte
が全く更新されていないために不正な状態になることがありました。
変更内容の概要
このコミットでは、ReadSlice
関数がどのように終了するかにかかわらず、常に読み込んだバイト列の最後のバイトがb.lastByte
に設定されるようにロジックが変更されました。具体的には、ReadSlice
のループ内でline
変数に結果が代入された後、ループを抜けた直後(関数のreturn
文の直前)に、line
の最後のバイトをb.lastByte
に設定する処理が追加されました。
これにより、ReadSlice
が正常に終了した場合でも、ErrBufferFull
で終了した場合でも、あるいはb.err
が設定されて終了した場合でも、常にb.lastByte
が正しく更新されることが保証されます。
ReadLine
への影響
ReadLine
関数は、内部的にReadSlice('\n')
を呼び出して行を読み込みます。したがって、ReadSlice
のlastByte
更新ロジックが修正されたことで、ReadLine
が返した後にUnreadByte
を呼び出しても、期待通りに動作するようになります。コミットメッセージにもあるように、「ReadLine
の後にUnreadByte
を呼び出すと、常に最後に読み込んだバイト(おそらく行末の一部である文字)を、ReadLine
が返した行の一部でなくても、アンリードする」という動作が保証されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/bufio/bufio.go
のReadSlice
関数と、その動作を検証するためのテストコードsrc/pkg/bufio/bufio_test.go
にあります。
src/pkg/bufio/bufio.go
ReadSlice
関数において、line
変数のスコープが変更され、関数の最後にlastByte
を更新する共通のロジックが追加されました。
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -274,26 +274,36 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
for {
// Search buffer.
if i := bytes.IndexByte(b.buf[b.r:b.w], delim); i >= 0 {
- line := b.buf[b.r : b.r+i+1]
+ line = b.buf[b.r : b.r+i+1]
b.r += i + 1
- return line, nil
+ break
}
// Pending error?
if b.err != nil {
- line := b.buf[b.r:b.w]
+ line = b.buf[b.r:b.w]
b.r = b.w
- return line, b.readErr()
+ err = b.readErr()
+ break
}
// Buffer full?
if n := b.Buffered(); n >= len(b.buf) {
b.r = b.w
- return b.buf, ErrBufferFull
+ line = b.buf
+ err = ErrBufferFull
+ break
}
b.fill() // buffer is not full
}
+
+ // Handle last byte, if any.
+ if i := len(line) - 1; i >= 0 {
+ b.lastByte = int(line[i])
+ }
+
+ return
}
// ReadLine is a low-level line-reading primitive. Most callers should use
@@ -309,6 +319,9 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, isPrefix bool, err error) {
//
// The text returned from ReadLine does not include the line end ("\r\n" or "\n").
// No indication or error is given if the input ends without a final line end.
+// Calling UnreadByte after ReadLine will always unread the last byte read
+// (possibly a character belonging to the line end) even if that byte is not
+// part of the line returned by ReadLine.
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {
line, err = b.ReadSlice('\n')
if err == ErrBufferFull {
src/pkg/bufio/bufio_test.go
TestUnreadByteOthers
という新しいテスト関数が追加され、ReadBytes
, ReadSlice
, ReadString
といった複数のバイトを読み込む関数とUnreadByte
の組み合わせが正しく動作するかを検証しています。
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -348,6 +348,62 @@ func TestUnreadByteMultiple(t *testing.T) {
}\n
}\n
\n
+func TestUnreadByteOthers(t *testing.T) {
+ // A list of readers to use in conjuction with UnreadByte.
+ var readers = []func(*Reader, byte) ([]byte, error){
+ (*Reader).ReadBytes,
+ (*Reader).ReadSlice,
+ func(r *Reader, delim byte) ([]byte, error) {
+ data, err := r.ReadString(delim)
+ return []byte(data), err
+ },
+ // ReadLine doesn't fit the data/pattern easily
+ // so we leave it out. It should be covered via
+ // the ReadSlice test since ReadLine simply calls
+ // ReadSlice, and it's that function that handles
+ // the last byte.
+ }
+
+ // Try all readers with UnreadByte.
+ for rno, read := range readers {
+ // Some input data that is longer than the minimum reader buffer size.
+ const n = 10
+ var buf bytes.Buffer
+ for i := 0; i < n; i++ {
+ buf.WriteString("abcdefg")
+ }
+
+ r := NewReaderSize(&buf, minReadBufferSize)
+ readTo := func(delim byte, want string) {
+ data, err := read(r, delim)
+ if err != nil {
+ t.Fatalf("#%d: unexpected error reading to %c: %v", rno, delim, err)
+ }
+ if got := string(data); got != want {
+ t.Fatalf("#%d: got %q, want %q", rno, got, want)
+ }
+ }
+
+ // Read the data with occasional UnreadByte calls.
+ for i := 0; i < n; i++ {
+ readTo('d', "abcd")
+ for j := 0; j < 3; j++ {
+ if err := r.UnreadByte(); err != nil {
+ t.Fatalf("#%d: unexpected error on UnreadByte: %v", rno, err)
+ }
+ readTo('d', "d")
+ }
+ readTo('g', "efg")
+ }
+
+ // All data should have been read.
+ _, err := r.ReadByte()
+ if err != io.EOF {
+ t.Errorf("#%d: got error %v; want EOF", rno, err)
+ }
+ }
+}
+
// Test that UnreadRune fails if the preceding operation was not a ReadRune.
func TestUnreadRuneError(t *testing.T) {
buf := make([]byte, 3) // All runes in this test are 3 bytes long
コアとなるコードの解説
ReadSlice
関数の変更
変更前のReadSlice
関数では、line := b.buf[b.r : b.r+i+1]
のようにline
変数がループ内で:=
(ショート変数宣言)を使って宣言されていました。これは、line
変数のスコープがそのif
ブロック内に限定されることを意味します。そのため、ループの最後にb.lastByte = int(line[i])
のような共通の処理を追加しようとしても、line
変数がスコープ外になってしまいアクセスできませんでした。
このコミットでは、line
変数を関数のシグネチャで宣言するように変更し、ループ内では=
(代入)を使用するように修正しました。
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
// ...
for {
// Search buffer.
if i := bytes.IndexByte(b.buf[b.r:b.w], delim); i >= 0 {
line = b.buf[b.r : b.r+i+1] // ここが := から = に変更
b.r += i + 1
break // 以前はここで return していた
}
// Pending error?
if b.err != nil {
line = b.buf[b.r:b.w] // ここが := から = に変更
b.r = b.w
err = b.readErr() // 以前はここで return していた
break
}
// Buffer full?
if n := b.Buffered(); n >= len(b.buf) {
b.r = b.w
line = b.buf // ここが := から = に変更
err = ErrBufferFull // 以前はここで return していた
break
}
b.fill() // buffer is not full
}
// Handle last byte, if any.
// ループを抜けた後に、共通のロジックで lastByte を更新
if i := len(line) - 1; i >= 0 {
b.lastByte = int(line[i])
}
return // 最後にまとめて return
}
この変更により、ReadSlice
がどのパスを通って終了しても、line
変数には常に最後に読み込んだバイト列が格納され、その最後のバイトがb.lastByte
に設定されることが保証されます。これにより、UnreadByte
が常に正しいバイトをバッファに戻せるようになります。
TestUnreadByteOthers
テストの追加
この新しいテストは、ReadBytes
、ReadSlice
、ReadString
の各関数がUnreadByte
と組み合わせて正しく動作するかを検証するために追加されました。
テストでは、まずreaders
というスライスに、テスト対象となる読み込み関数(ReadBytes
、ReadSlice
、ReadString
のラッパー)を格納します。ReadLine
は、内部的にReadSlice
を呼び出すため、ReadSlice
のテストでカバーされると判断され、直接はテスト対象に含まれていません。
各読み込み関数に対して、以下の手順でテストが実行されます。
- 十分な長さの入力データ("abcdefg"を10回繰り返したもの)を持つ
bytes.Buffer
を作成します。 - その
bytes.Buffer
を元にbufio.Reader
を初期化します。 readTo
というヘルパー関数を定義し、指定された区切り文字まで読み込み、結果が期待通りであることを検証します。- メインのループでは、入力データを
readTo
で読み込みます。 - 途中で
r.UnreadByte()
を複数回呼び出し、その後に再度readTo
で読み込みを行うことで、UnreadByte
が正しく機能しているかを確認します。例えば、"abcd"を読み込んだ後、3回UnreadByte
を呼び出し、その後"d"を読み込むことで、"d"が正しくバッファに戻され、再度読み込まれることを検証します。 - すべてのデータが読み込まれた後、
io.EOF
が返されることを確認し、余分なデータが残っていないことを保証します。
このテストは、様々な読み込み関数とUnreadByte
の複雑な相互作用を網羅的に検証することで、このコミットによる修正が意図通りに機能していることを保証します。
関連リンク
- Go Issue #7844: https://github.com/golang/go/issues/7844
- Gerrit Change-ID: https://golang.org/cl/90620045
参考にした情報源リンク
- Go言語
bufio
パッケージ公式ドキュメント: https://pkg.go.dev/bufio - Go言語
bytes
パッケージ公式ドキュメント: https://pkg.go.dev/bytes - Go言語
io
パッケージ公式ドキュメント: https://pkg.go.dev/io
[インデックス 19229] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージの改善に関するものです。bufio
パッケージは、I/O操作をバッファリングすることで効率化するための機能を提供します。具体的には、bufio.Reader
は入力ストリームからの読み込みをバッファリングし、bufio.Writer
は出力ストリームへの書き込みをバッファリングします。
このコミットの主な目的は、bufio.Reader
の様々な読み込み関数(ReadBytes
, ReadSlice
, ReadString
など)が、UnreadByte
メソッドと組み合わせて使用された際に、常に期待通りに動作するように修正することです。
コミット
bufio: make all read functions UnreadByte-friendly
Fixes #7844.
LGTM=crawshaw
R=golang-codereviews, crawshaw
CC=golang-codereviews
https://golang.org/cl/90620045
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/9144d8752a4a1cdee1a9c7ebd2c5ae2739293509
元コミット内容
bufio: make all read functions UnreadByte-friendly
このコミットは、bufio
パッケージ内のすべての読み込み関数がUnreadByte
メソッドと互換性を持つように修正します。これは、GoのIssue #7844で報告された問題を解決するためのものです。
変更の背景
Go言語のbufio
パッケージは、効率的なI/O操作のためにバッファリング機能を提供します。bufio.Reader
には、ReadByte
で読み込んだ最後のバイトをバッファに戻すためのUnreadByte
メソッドが存在します。しかし、ReadSlice
やReadBytes
、ReadString
といった、複数のバイトを一度に読み込む関数を使用した後にUnreadByte
を呼び出すと、期待通りの動作をしないという問題が報告されていました(Issue #7844)。
この問題は、UnreadByte
が正しく機能するために必要な「最後に読み込んだバイト」の情報が、これらの関数によって適切に更新されていなかったことに起因します。UnreadByte
は、bufio.Reader
内部のlastByte
フィールドに格納された値を利用して、バッファにバイトを戻します。しかし、ReadSlice
などの関数が、読み込み処理の途中でエラーが発生したり、特定の条件で早期に終了したりした場合に、lastByte
が正しく設定されないことがありました。
この不整合は、bufio.Reader
のAPI契約の一部であるUnreadByte
の信頼性を損ねるものであり、ユーザーが予期しない動作に遭遇する可能性がありました。そのため、すべての読み込み関数がUnreadByte
の期待する動作をサポートするように修正する必要がありました。
前提知識の解説
bufio
パッケージ
bufio
パッケージは、Go言語におけるバッファリングされたI/Oを提供する標準ライブラリです。これにより、ディスクI/OやネットワークI/Oなどの低レベルな読み書き操作の回数を減らし、アプリケーションのパフォーマンスを向上させることができます。
bufio.Reader
: 入力ストリームからの読み込みをバッファリングします。これにより、一度に大量のデータを読み込み、アプリケーションが必要とする分だけをバッファから提供することで、システムコールを削減します。bufio.Writer
: 出力ストリームへの書き込みをバッファリングします。同様に、書き込み操作をまとめて実行することで、効率を高めます。
UnreadByte
メソッド
bufio.Reader
のUnreadByte
メソッドは、直前にReadByte
、ReadRune
、またはその他の読み込み関数によって読み込まれた最後のバイトを、内部バッファの先頭に戻す機能を提供します。これにより、プログラムは読み込んだバイトを「取り消し」て、再度読み込み直すことができます。これは、例えばパーサーが特定の区切り文字を読み込んだ後、その区切り文字を次の処理のためにバッファに戻したい場合などに有用です。
UnreadByte
が正しく機能するためには、bufio.Reader
が最後に読み込んだバイトを正確に記憶している必要があります。この情報は、bufio.Reader
構造体の内部フィールドであるlastByte
に格納されます。
関連する読み込み関数
ReadByte()
: 1バイトを読み込みます。ReadRune()
: 1つのUnicodeルーン(文字)を読み込みます。ReadSlice(delim byte)
: 指定された区切り文字(delim
)が見つかるまでバイトを読み込み、その区切り文字を含むバイトスライスを返します。ReadBytes(delim byte)
:ReadSlice
と同様に区切り文字まで読み込みますが、バイトスライスをコピーして返します。ReadString(delim byte)
:ReadBytes
と同様に区切り文字まで読み込みますが、結果を文字列として返します。ReadLine()
: 改行文字(\n
または\r\n
)までを読み込み、その行のバイトスライスを返します。改行文字自体は返されるスライスには含まれません。
lastByte
フィールド
bufio.Reader
構造体には、lastByte
という内部フィールドがあります。このフィールドは、UnreadByte
メソッドが機能するために非常に重要です。ReadByte
やReadRune
などの読み込み関数がバイトを読み込むたびに、その読み込んだバイトがlastByte
に格納されます。UnreadByte
が呼び出されると、このlastByte
に格納されたバイトがバッファに戻されます。
問題は、ReadSlice
やReadBytes
、ReadString
といった複数のバイトを読み込む関数が、読み込みの途中でlastByte
を適切に更新していなかったり、更新のタイミングが不適切だったりしたことです。これにより、これらの関数が終了した後にUnreadByte
を呼び出すと、期待とは異なるバイトが戻されるか、エラーが発生する可能性がありました。
技術的詳細
このコミットの技術的な核心は、bufio.Reader
のReadSlice
関数におけるlastByte
フィールドの更新ロジックの修正にあります。
UnreadByte
が正しく機能するための条件
UnreadByte
メソッドは、直前の読み込み操作によってバッファから取り出された最後のバイトを正確に「記憶」している必要があります。この「記憶」は、bufio.Reader
の内部状態であるlastByte
フィールドによって実現されます。したがって、すべての読み込み関数は、読み込みが完了した時点で、その操作によって消費された最後のバイトをlastByte
に設定する責任があります。
変更前の問題点
変更前のReadSlice
関数は、複数のリターンパスを持っていました。例えば、区切り文字が見つかった場合、バッファが一杯になった場合、または読み込み中にエラーが発生した場合などです。これらの異なるリターンパスにおいて、lastByte
が常に適切に更新されるとは限りませんでした。
特に、ReadSlice
が区切り文字を見つけてbreak
する際や、エラーが発生してbreak
する際に、line
変数に格納されたバイト列の最後のバイトがlastByte
に設定される前に関数が終了してしまう可能性がありました。これにより、UnreadByte
が呼び出されたときに、古いlastByte
の値が使用されるか、あるいはlastByte
が全く更新されていないために不正な状態になることがありました。
変更内容の概要
このコミットでは、ReadSlice
関数がどのように終了するかにかかわらず、常に読み込んだバイト列の最後のバイトがb.lastByte
に設定されるようにロジックが変更されました。具体的には、ReadSlice
のループ内でline
変数に結果が代入された後、ループを抜けた直後(関数のreturn
文の直前)に、line
の最後のバイトをb.lastByte
に設定する処理が追加されました。
これにより、ReadSlice
が正常に終了した場合でも、ErrBufferFull
で終了した場合でも、あるいはb.err
が設定されて終了した場合でも、常にb.lastByte
が正しく更新されることが保証されます。
ReadLine
への影響
ReadLine
関数は、内部的にReadSlice('\n')
を呼び出して行を読み込みます。したがって、ReadSlice
のlastByte
更新ロジックが修正されたことで、ReadLine
が返した後にUnreadByte
を呼び出しても、期待通りに動作するようになります。コミットメッセージにもあるように、「ReadLine
の後にUnreadByte
を呼び出すと、常に最後に読み込んだバイト(おそらく行末の一部である文字)を、ReadLine
が返した行の一部でなくても、アンリードする」という動作が保証されます。
コアとなるコードの変更箇所
このコミットにおける主要なコード変更は、src/pkg/bufio/bufio.go
のReadSlice
関数と、その動作を検証するためのテストコードsrc/pkg/bufio/bufio_test.go
にあります。
src/pkg/bufio/bufio.go
ReadSlice
関数において、line
変数のスコープが変更され、関数の最後にlastByte
を更新する共通のロジックが追加されました。
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -274,26 +274,36 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
for {
// Search buffer.
if i := bytes.IndexByte(b.buf[b.r:b.w], delim); i >= 0 {
- line := b.buf[b.r : b.r+i+1]
+ line = b.buf[b.r : b.r+i+1]
b.r += i + 1
- return line, nil
+ break
}
// Pending error?
if b.err != nil {
- line := b.buf[b.r:b.w]
+ line = b.buf[b.r:b.w]
b.r = b.w
- return line, b.readErr()
+ err = b.readErr()
+ break
}
// Buffer full?
if n := b.Buffered(); n >= len(b.buf) {
b.r = b.w
- return b.buf, ErrBufferFull
+ line = b.buf
+ err = ErrBufferFull
+ break
}
b.fill() // buffer is not full
}
+
+ // Handle last byte, if any.
+ if i := len(line) - 1; i >= 0 {
+ b.lastByte = int(line[i])
+ }
+
+ return
}
// ReadLine is a low-level line-reading primitive. Most callers should use
@@ -309,6 +319,9 @@ func (b *Reader) ReadSlice(delim byte) (line []byte, isPrefix bool, err error) {
//
// The text returned from ReadLine does not include the line end ("\r\n" or "\n").
// No indication or error is given if the input ends without a final line end.
+// Calling UnreadByte after ReadLine will always unread the last byte read
+// (possibly a character belonging to the line end) even if that byte is not
+// part of the line returned by ReadLine.
func (b *Reader) ReadLine() (line []byte, isPrefix bool, err error) {
line, err = b.ReadSlice('\n')
if err == ErrBufferFull {
src/pkg/bufio/bufio_test.go
TestUnreadByteOthers
という新しいテスト関数が追加され、ReadBytes
, ReadSlice
, ReadString
といった複数のバイトを読み込む関数とUnreadByte
の組み合わせが正しく動作するかを検証しています。
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -348,6 +348,62 @@ func TestUnreadByteMultiple(t *testing.T) {
}\n
}\n
\n
+func TestUnreadByteOthers(t *testing.T) {
+ // A list of readers to use in conjuction with UnreadByte.
+ var readers = []func(*Reader, byte) ([]byte, error){
+ (*Reader).ReadBytes,
+ (*Reader).ReadSlice,
+ func(r *Reader, delim byte) ([]byte, error) {
+ data, err := r.ReadString(delim)
+ return []byte(data), err
+ },
+ // ReadLine doesn't fit the data/pattern easily
+ // so we leave it out. It should be covered via
+ // the ReadSlice test since ReadLine simply calls
+ // ReadSlice, and it's that function that handles
+ // the last byte.
+ }
+
+ // Try all readers with UnreadByte.
+ for rno, read := range readers {
+ // Some input data that is longer than the minimum reader buffer size.
+ const n = 10
+ var buf bytes.Buffer
+ for i := 0; i < n; i++ {
+ buf.WriteString("abcdefg")
+ }
+
+ r := NewReaderSize(&buf, minReadBufferSize)
+ readTo := func(delim byte, want string) {
+ data, err := read(r, delim)
+ if err != nil {
+ t.Fatalf("#%d: unexpected error reading to %c: %v", rno, delim, err)
+ }
+ if got := string(data); got != want {
+ t.Fatalf("#%d: got %q, want %q", rno, got, want)
+ }
+ }
+
+ // Read the data with occasional UnreadByte calls.
+ for i := 0; i < n; i++ {
+ readTo('d', "abcd")
+ for j := 0; j < 3; j++ {
+ if err := r.UnreadByte(); err != nil {
+ t.Fatalf("#%d: unexpected error on UnreadByte: %v", rno, err)
+ }
+ readTo('d', "d")
+ }
+ readTo('g', "efg")
+ }
+
+ // All data should have been read.
+ _, err := r.ReadByte()
+ if err != io.EOF {
+ t.Errorf("#%d: got error %v; want EOF", rno, err)
+ }
+ }
+}
+
// Test that UnreadRune fails if the preceding operation was not a ReadRune.
func TestUnreadRuneError(t *testing.T) {
buf := make([]byte, 3) // All runes in this test are 3 bytes long
コアとなるコードの解説
ReadSlice
関数の変更
変更前のReadSlice
関数では、line := b.buf[b.r : b.r+i+1]
のようにline
変数がループ内で:=
(ショート変数宣言)を使って宣言されていました。これは、line
変数のスコープがそのif
ブロック内に限定されることを意味します。そのため、ループの最後にb.lastByte = int(line[i])
のような共通の処理を追加しようとしても、line
変数がスコープ外になってしまいアクセスできませんでした。
このコミットでは、line
変数を関数のシグネチャで宣言するように変更し、ループ内では=
(代入)を使用するように修正しました。
func (b *Reader) ReadSlice(delim byte) (line []byte, err error) {
// ...
for {
// Search buffer.
if i := bytes.IndexByte(b.buf[b.r:b.w], delim); i >= 0 {
line = b.buf[b.r : b.r+i+1] // ここが := から = に変更
b.r += i + 1
break // 以前はここで return していた
}
// Pending error?
if b.err != nil {
line = b.buf[b.r:b.w] // ここが := から = に変更
b.r = b.w
err = b.readErr() // 以前はここで return していた
break
}
// Buffer full?
if n := b.Buffered(); n >= len(b.buf) {
b.r = b.w
line = b.buf // ここが := から = に変更
err = ErrBufferFull // 以前はここで return していた
break
}
b.fill() // buffer is not full
}
// Handle last byte, if any.
// ループを抜けた後に、共通のロジックで lastByte を更新
if i := len(line) - 1; i >= 0 {
b.lastByte = int(line[i])
}
return // 最後にまとめて return
}
この変更により、ReadSlice
がどのパスを通って終了しても、line
変数には常に最後に読み込んだバイト列が格納され、その最後のバイトがb.lastByte
に設定されることが保証されます。これにより、UnreadByte
が常に正しいバイトをバッファに戻せるようになります。
TestUnreadByteOthers
テストの追加
この新しいテストは、ReadBytes
、ReadSlice
、ReadString
の各関数がUnreadByte
と組み合わせて正しく動作するかを検証するために追加されました。
テストでは、まずreaders
というスライスに、テスト対象となる読み込み関数(ReadBytes
、ReadSlice
、ReadString
のラッパー)を格納します。ReadLine
は、内部的にReadSlice
を呼び出すため、ReadSlice
のテストでカバーされると判断され、直接はテスト対象に含まれていません。
各読み込み関数に対して、以下の手順でテストが実行されます。
- 十分な長さの入力データ("abcdefg"を10回繰り返したもの)を持つ
bytes.Buffer
を作成します。 - その
bytes.Buffer
を元にbufio.Reader
を初期化します。 readTo
というヘルパー関数を定義し、指定された区切り文字まで読み込み、結果が期待通りであることを検証します。- メインのループでは、入力データを
readTo
で読み込みます。 - 途中で
r.UnreadByte()
を複数回呼び出し、その後に再度readTo
で読み込みを行うことで、UnreadByte
が正しく機能しているかを確認します。例えば、"abcd"を読み込んだ後、3回UnreadByte
を呼び出し、その後"d"を読み込むことで、"d"が正しくバッファに戻され、再度読み込まれることを検証します。 - すべてのデータが読み込まれた後、
io.EOF
が返されることを確認し、余分なデータが残っていないことを保証します。
このテストは、様々な読み込み関数とUnreadByte
の複雑な相互作用を網羅的に検証することで、このコミットによる修正が意図通りに機能していることを保証します。
関連リンク
- Go Issue #7844: https://github.com/golang/go/issues/7844
- Gerrit Change-ID: https://golang.org/cl/90620045
参考にした情報源リンク
- Go言語
bufio
パッケージ公式ドキュメント: https://pkg.go.dev/bufio - Go言語
bytes
パッケージ公式ドキュメント: https://pkg.go.dev/bytes - Go言語
io
パッケージ公式ドキュメント: https://pkg.go.dev/io