[インデックス 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