[インデックス 10768] ファイルの概要
このコミットは、Go言語の標準ライブラリであるbufio
パッケージにおける、読み込みバッファの最小サイズに関する変更です。具体的には、bufio.Reader
が使用するバッファの最小サイズを16バイトに設定し、それに伴うテストコードの修正が含まれています。
src/pkg/bufio/bufio.go
:bufio
パッケージの主要な実装ファイルであり、Reader
構造体とNewReaderSize
関数が定義されています。このファイルでは、最小バッファサイズを定義する定数の追加と、NewReaderSize
関数におけるバッファサイズ検証ロジックの変更が行われています。src/pkg/bufio/bufio_test.go
:bufio
パッケージの単体テストファイルです。最小バッファサイズの変更に伴い、既存のテストケースが新しい制約と挙動に合わせて広範囲にわたって更新されています。
コミット
- コミットハッシュ:
64776da456682db445462e6d095de1b2b6652a8e
- 作者: Rob Pike r@golang.org
- コミット日時: 2011年12月13日 15:07:17 -0800
- コミットメッセージ:
bufio: make the minimum read buffer size 16 bytes. R=gri, rsc CC=golang-dev https://golang.org/cl/5485067
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/64776da456682db445462e6d095de1b2b6652a8e
元コミット内容
このコミットの目的は、「bufio
パッケージの読み込みバッファの最小サイズを16バイトにする」ことです。コミットメッセージには、レビュー担当者(R=gri, rsc
)とメーリングリスト(CC=golang-dev
)、そしてGoのコードレビューシステムであるGerritへのリンク(https://golang.org/cl/5485067
)が含まれています。これは、Goプロジェクトにおける典型的な開発ワークフローを示しており、変更がレビュープロセスを経て承認されたことを意味します。
変更の背景
Go言語のbufio
パッケージは、効率的なI/O操作のためにバッファリングを提供します。バッファリングは、ディスクやネットワークとの間でデータをやり取りする際のシステムコール回数を減らすことで、アプリケーションのパフォーマンスを大幅に向上させます。
bufio.Reader
を生成するNewReaderSize
関数は、以前は1バイトより大きい任意のバッファサイズを受け入れていました(size <= 1
というチェックがあったため、最小は2バイト)。しかし、非常に小さなバッファサイズ(例えば数バイト)を使用すると、バッファがすぐに満杯になり、頻繁なバッファの再充填が必要になります。これは、バッファリングの利点を打ち消し、かえってパフォーマンスを低下させる可能性があります。また、特定のI/O操作(例: ReadSlice
やPeek
)において、バッファが小さすぎるとErrBufferFull
のようなエラーが頻繁に発生し、アプリケーションロジックが複雑になる原因にもなりえます。
このコミットは、このような非効率性や潜在的な問題に対処するために導入されました。最小バッファサイズを16バイトに設定することで、bufio.Reader
が常に合理的なサイズのバッファで動作することを保証し、より予測可能で効率的なI/Oパフォーマンスを提供することを目指しています。16バイトというサイズは、多くのシステムで効率的なメモリアクセスやアライメントに適した、2のべき乗のサイズとして選ばれたと考えられます。
前提知識の解説
- Go言語の
bufio
パッケージ: Goの標準ライブラリの一部で、バッファリングされたI/O機能を提供します。これにより、低レベルのI/O操作(os.File
やnet.Conn
など)を直接扱うよりも効率的にデータを読み書きできます。bufio.Reader
は読み込み操作を、bufio.Writer
は書き込み操作をバッファリングします。 - バッファリング (Buffering): データが最終的な目的地に到達する前に、一時的にメモリ(バッファ)に蓄えられるプロセスです。I/O操作はCPUにとってコストの高い操作であり、システムコールを頻繁に発行するとオーバーヘッドが大きくなります。バッファリングにより、一度に大量のデータをまとめて読み書きすることで、システムコールの回数を減らし、全体的なスループットを向上させます。
io.Reader
インターフェース: Go言語のio
パッケージで定義されている基本的なインターフェースの一つです。Read(p []byte) (n int, err error)
メソッドを持ち、任意のデータソースからバイト列を読み込むための抽象化を提供します。bufio.Reader
は、このio.Reader
インターフェースをラップしてバッファリング機能を追加します。NewReaderSize
関数:bufio
パッケージのコンストラクタ関数で、指定されたio.Reader
と、そのReader
が使用する内部バッファのサイズ(バイト単位)を受け取って、新しいbufio.Reader
インスタンスを返します。この関数を使うことで、バッファのサイズをアプリケーションの要件に合わせて調整できます。ErrBufferFull
エラー:bufio
パッケージで定義されているエラー定数です。bufio.Reader
のReadSlice
やPeek
などのメソッドが、要求された操作を完了するために十分なバッファスペースがない場合に返されます。例えば、区切り文字が見つかる前にバッファが満杯になった場合などに発生します。
技術的詳細
このコミットの技術的な核心は、bufio.Reader
の最小バッファサイズを強制することにあります。
-
minReadBufferSize
定数の導入:src/pkg/bufio/bufio.go
にconst minReadBufferSize = 16
という新しい定数が追加されました。この定数は、bufio.Reader
が許容する最小のバッファサイズを明示的に16バイトと定義しています。これにより、マジックナンバー(コード中に直接埋め込まれた意味不明な数値)を避け、コードの可読性と保守性が向上します。 -
NewReaderSize
関数のバッファサイズ検証ロジックの変更:NewReaderSize
関数の内部で、引数として渡されるsize
(バッファサイズ)の検証ロジックが変更されました。- 変更前:
if size <= 1 { return nil, BufSizeError(size) }
これは、バッファサイズが1バイト以下の場合にエラーを返すことを意味し、実質的な最小バッファサイズは2バイトでした。 - 変更後:
if size < minReadBufferSize { return nil, BufSizeError(size) }
この変更により、size
が新しく定義されたminReadBufferSize
(16バイト)よりも小さい場合にBufSizeError
を返すようになりました。これにより、開発者が意図的に、または誤って、非効率な小さなバッファサイズを設定することを防ぎます。関数のコメントも「which must be greater than one」から「which must be at least 16 bytes」に更新され、新しい制約が明確に示されています。
- 変更前:
-
テストケースの広範な更新:
src/pkg/bufio/bufio_test.go
では、この最小バッファサイズの変更に合わせて、多数のテストケースが修正されました。bufsizes
配列の変更: テストスイートが様々なバッファサイズでbufio.Reader
の挙動を検証するために使用するbufsizes
配列から、以前の最小値である2
が削除され、代わりに新しい最小値であるminReadBufferSize
(16)が追加されました。これにより、テストが新しい制約の下で適切に実行されることが保証されます。- 既存テストの調整:
TestBufferFull
、TestPeek
、TestLineTooLong
、TestReadEmptyBuffer
、TestLinesAfterRead
、TestReadLineNewlines
など、多くのテスト関数で、ハードコードされていた小さなバッファサイズ(例:5
,4
,3
,10
)がminReadBufferSize
に置き換えられました。また、これらのテストで使用される入力データや期待される出力も、新しい最小バッファサイズに合わせて調整されています。例えば、TestLineTooLong
では、テストデータの生成ロジックがminReadBufferSize
に基づいて動的に変更され、より現実的なシナリオでErrBufferFull
の挙動を検証できるようになりました。これにより、bufio
パッケージの各種機能が、16バイトの最小バッファサイズという新しい制約の下でも期待通りに動作することを確認しています。
これらの変更は、bufio
パッケージの堅牢性とパフォーマンスを向上させることを目的としています。小さなバッファサイズが引き起こす可能性のあるエッジケースや非効率性を排除し、より予測可能で効率的なI/O操作を保証します。
コアとなるコードの変更箇所
src/pkg/bufio/bufio.go
--- a/src/pkg/bufio/bufio.go
+++ b/src/pkg/bufio/bufio.go
@@ -52,12 +52,14 @@ type Reader struct {
lastRuneSize int
}
+const minReadBufferSize = 16
+
// NewReaderSize creates a new Reader whose buffer has the specified size,
-// which must be greater than one. If the argument io.Reader is already a
+// which must be at least 16 bytes. If the argument io.Reader is already a
// Reader with large enough size, it returns the underlying Reader.
// It returns the Reader and any error.
func NewReaderSize(rd io.Reader, size int) (*Reader, error) {
-\tif size <= 1 {\n+\tif size < minReadBufferSize {\n \t\treturn nil, BufSizeError(size)\n \t}\n \t// Is it already a Reader?
src/pkg/bufio/bufio_test.go
--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -135,9 +135,10 @@ var bufreaders = []bufReader{
{"lines", readLines},
}
+const minReadBufferSize = 16
+
var bufsizes = []int{
-\t2, 3, 4, 5, 6, 7, 8, 9, 10,
-\t23, 32, 46, 64, 93, 128, 1024, 4096,
+\tminReadBufferSize, 23, 32, 46, 64, 93, 128, 1024, 4096,
}
func TestReader(t *testing.T) {
@@ -514,27 +515,32 @@ func TestWriteString(t *testing.T) {
}
func TestBufferFull(t *testing.T) {
-\tbuf, _ := NewReaderSize(strings.NewReader("hello, world"), 5)
-\tline, err := buf.ReadSlice(',')
-\tif string(line) != "hello" || err != ErrBufferFull {
+\tconst longString = "And now, hello, world! It is the time for all good men to come to the aid of their party"
+\tbuf, err := NewReaderSize(strings.NewReader(longString), minReadBufferSize)
+\tif err != nil {
+\t\tt.Fatal("NewReaderSize:", err)
+\t}
+\tline, err := buf.ReadSlice('!')
+\tif string(line) != "And now, hello, " || err != ErrBufferFull {
\t\tt.Errorf("first ReadSlice(,) = %q, %v", line, err)
\t}
-\tline, err = buf.ReadSlice(',')
-\tif string(line) != "," || err != nil {
+\tline, err = buf.ReadSlice('!')
+\tif string(line) != "world!" || err != nil {
\t\tt.Errorf("second ReadSlice(,) = %q, %v", line, err)
\t}
}
func TestPeek(t *testing.T) {
p := make([]byte, 10)
-\tbuf, _ := NewReaderSize(strings.NewReader("abcdefghij"), 4)
+\t// string is 16 (minReadBufferSize) long.
+\tbuf, _ := NewReaderSize(strings.NewReader("abcdefghijklmnop"), minReadBufferSize)
\tif s, err := buf.Peek(1); string(s) != "a" || err != nil {
\t\tt.Fatalf("want %q got %q, err=%v", "a", string(s), err)
\t}
\tif s, err := buf.Peek(4); string(s) != "abcd" || err != nil {
\t\tt.Fatalf("want %q got %q, err=%v", "abcd", string(s), err)
\t}
-\tif _, err := buf.Peek(5); err != ErrBufferFull {
+\tif _, err := buf.Peek(32); err != ErrBufferFull {
\t\tt.Fatalf("want ErrBufFull got %v", err)
\t}
\tif _, err := buf.Read(p[0:3]); string(p[0:3]) != "abc" || err != nil {
@@ -552,8 +558,8 @@ func TestPeek(t *testing.T) {
\tif s, err := buf.Peek(4); string(s) != "ghij" || err != nil {
\t\tt.Fatalf("want %q got %q, err=%v", "ghij", string(s), err)
\t}
-\tif _, err := buf.Read(p[0:4]); string(p[0:4]) != "ghij" || err != nil {
-\t\tt.Fatalf("want %q got %q, err=%v", "ghij", string(p[0:3]), err)
+\tif _, err := buf.Read(p[0:]); string(p[0:]) != "ghijklmnop" || err != nil {
+\t\tt.Fatalf("want %q got %q, err=%v", "ghijklmnop", string(p[0:minReadBufferSize]), err)
\t}
\tif s, err := buf.Peek(0); string(s) != "" || err != nil {
\t\tt.Fatalf("want %q got %q, err=%v", "", string(s), err)
@@ -635,19 +641,25 @@ func TestReadLine(t *testing.T) {
}
func TestLineTooLong(t *testing.T) {
-\tbuf := bytes.NewBuffer([]byte("aaabbbcc\n"))
-\tl, _ := NewReaderSize(buf, 3)
+\tdata := make([]byte, 0)
+\tfor i := 0; i < minReadBufferSize*5/2; i++ {
+\t\tdata = append(data, '0'+byte(i%10))
+\t}
+\tbuf := bytes.NewBuffer(data)
+\tl, _ := NewReaderSize(buf, minReadBufferSize)
\tline, isPrefix, err := l.ReadLine()
-\tif !isPrefix || !bytes.Equal(line, []byte("aaa")) || err != nil {
-\t\tt.Errorf("bad result for first line: %x %s", line, err)
+\tif !isPrefix || !bytes.Equal(line, data[:minReadBufferSize]) || err != nil {
+\t\tt.Errorf("bad result for first line: got %q want %q %v", line, data[:minReadBufferSize], err)
\t}
+\tdata = data[len(line):]
\tline, isPrefix, err = l.ReadLine()
-\tif !isPrefix || !bytes.Equal(line, []byte("bbb")) || err != nil {
-\t\tt.Errorf("bad result for second line: %x", line)
+\tif !isPrefix || !bytes.Equal(line, data[:minReadBufferSize]) || err != nil {
+\t\tt.Errorf("bad result for second line: got %q want %q %v", line, data[:minReadBufferSize], err)
\t}
+\tdata = data[len(line):]
\tline, isPrefix, err = l.ReadLine()
-\tif isPrefix || !bytes.Equal(line, []byte("cc")) || err != nil {
-\t\tt.Errorf("bad result for third line: %x", line)
+\tif isPrefix || !bytes.Equal(line, data[:minReadBufferSize/2]) || err != nil {
+\t\tt.Errorf("bad result for third line: got %q want %q %v", line, data[:minReadBufferSize/2], err)
\t}
\tline, isPrefix, err = l.ReadLine()
\tif isPrefix || err == nil {
@@ -656,8 +668,8 @@ func TestLineTooLong(t *testing.T) {
}
func TestReadAfterLines(t *testing.T) {
-\tline1 := "line1"
-\trestData := "line2\nline 3\n"
+\tline1 := "this is line1"
+\trestData := "this is line2\nthis is line 3\n"
\tinbuf := bytes.NewBuffer([]byte(line1 + "\n" + restData))
\toutbuf := new(bytes.Buffer)
\tmaxLineLength := len(line1) + len(restData)/2
@@ -676,7 +688,7 @@ func TestReadAfterLines(t *testing.T) {
}
func TestReadEmptyBuffer(t *testing.T) {
-\tl, _ := NewReaderSize(bytes.NewBuffer(nil), 10)
+\tl, _ := NewReaderSize(bytes.NewBuffer(nil), minReadBufferSize)
\tline, isPrefix, err := l.ReadLine()
\tif err != io.EOF {
\t\tt.Errorf("expected EOF from ReadLine, got '%s' %t %s", line, isPrefix, err)
@@ -684,7 +696,7 @@ func TestReadEmptyBuffer(t *testing.T) {
}
func TestLinesAfterRead(t *testing.T) {
-\tl, _ := NewReaderSize(bytes.NewBuffer([]byte("foo")), 10)
+\tl, _ := NewReaderSize(bytes.NewBuffer([]byte("foo")), minReadBufferSize)
\t_, err := ioutil.ReadAll(l)
\tif err != nil {
\t\tt.Error(err)
@@ -715,34 +727,19 @@ type readLineResult struct {
}
var readLineNewlinesTests = []struct {
-\tinput string
-\tbufSize int
-\texpect []readLineResult
+\tinput string
+\texpect []readLineResult
}{
-\t{"h\r\nb\r\n", 2, []readLineResult{
-\t\t{[]byte("h"), true, nil},
-\t\t{nil, false, nil},
-\t\t{[]byte("b"), true, nil},
-\t\t{nil, false, nil},
-\t\t{nil, false, io.EOF},
-\t}},
-\t{"hello\r\nworld\r\n", 6, []readLineResult{
-\t\t{[]byte("hello"), true, nil},
+\t{"012345678901234\r\n012345678901234\r\n", []readLineResult{
+\t\t{[]byte("012345678901234"), true, nil},
\t\t{nil, false, nil},
-\t\t{[]byte("world"), true, nil},
+\t\t{[]byte("012345678901234"), true, nil},
\t\t{nil, false, nil},
\t\t{nil, false, io.EOF},
\t}},
-\t{"hello\rworld\r", 6, []readLineResult{
-\t\t{[]byte("hello"), true, nil},
-\t\t{[]byte("\rworld"), true, nil},
-\t\t{[]byte("\r"), false, nil},
-\t\t{nil, false, io.EOF},
-\t}},
-\t{"h\ri\r\n\r", 2, []readLineResult{
-\t\t{[]byte("h"), true, nil},
-\t\t{[]byte("\ri"), true, nil},
-\t\t{nil, false, nil},
+\t{"0123456789012345\r012345678901234\r", []readLineResult{
+\t\t{[]byte("0123456789012345"), true, nil},
+\t\t{[]byte("\r012345678901234"), true, nil},
\t\t{[]byte("\r"), false, nil},
\t\t{nil, false, io.EOF},
\t}},
@@ -750,12 +747,12 @@ var readLineNewlinesTests = []struct {
func TestReadLineNewlines(t *testing.T) {
for _, e := range readLineNewlinesTests {
-\t\ttestReadLineNewlines(t, e.input, e.bufSize, e.expect)
+\t\ttestReadLineNewlines(t, e.input, e.expect)
}
}
-func testReadLineNewlines(t *testing.T, input string, bufSize int, expect []readLineResult) {
-\tb, err := NewReaderSize(strings.NewReader(input), bufSize)
+func testReadLineNewlines(t *testing.T, input string, expect []readLineResult) {
+\tb, err := NewReaderSize(strings.NewReader(input), minReadBufferSize)
\tif err != nil {
\t\tt.Fatal(err)
\t}
コアとなるコードの解説
src/pkg/bufio/bufio.go
const minReadBufferSize = 16
: この行は、bufio.Reader
が内部で使用するバッファの最小サイズを16バイトと定義しています。これは、バッファリングの効率を確保し、非常に小さなバッファサイズによるパフォーマンス低下を防ぐための閾値となります。NewReaderSize
関数の変更:- 変更前は、
size <= 1
という条件でバッファサイズが1バイト以下の場合にエラーを返していました。これは、実質的に最小バッファサイズが2バイトであることを意味します。 - 変更後は、
size < minReadBufferSize
という条件に変わり、size
が新しく定義されたminReadBufferSize
(16バイト)よりも小さい場合にエラーを返すようになりました。これにより、開発者が意図せず非効率な小さなバッファサイズを設定してしまうことを防ぎ、bufio.Reader
が常に合理的なサイズのバッファで動作することを保証します。 - 関数のコメントも更新され、新しい最小バッファサイズ(16バイト)の要件が明確に示されています。
- 変更前は、
src/pkg/bufio/bufio_test.go
const minReadBufferSize = 16
: テストファイルにも同様にminReadBufferSize
定数が定義されています。これにより、テストコードが本体のコードと同じ最小バッファサイズを参照し、一貫性を保つことができます。bufsizes
配列の更新:- 以前は
2
から始まる様々なバッファサイズがテストされていましたが、この変更により2
が削除され、代わりにminReadBufferSize
(16)が追加されました。これは、テストスイートが新しい最小バッファサイズ制約の下で適切に動作するかを検証するために重要です。
- 以前は
- 各テスト関数の調整:
TestBufferFull
、TestPeek
、TestLineTooLong
、TestReadEmptyBuffer
、TestLinesAfterRead
、TestReadLineNewlines
など、多くのテスト関数で、以前は固定値や小さな値で設定されていたNewReaderSize
のバッファサイズがminReadBufferSize
に置き換えられています。- これらのテストでは、入力データや期待される結果も、新しい最小バッファサイズに合わせて調整されています。例えば、
TestLineTooLong
では、生成されるテストデータの長さがminReadBufferSize
に基づいて動的に決定されるようになり、より堅牢なテストが可能になっています。これにより、bufio
パッケージの各種機能が、16バイトの最小バッファサイズという新しい制約の下でも期待通りに動作することを確認しています。特に、ErrBufferFull
の発生条件や、ReadLine
が返すプレフィックスの挙動などが、新しいバッファサイズで正しく検証されるようになっています。
これらのテストの変更は、コードの変更が意図した通りに機能し、既存の機能が新しい制約の下でも壊れていないことを保証するための重要なステップです。
関連リンク
- Go言語の
bufio
パッケージのドキュメント: https://pkg.go.dev/bufio - Go言語の
io
パッケージのドキュメント: https://pkg.go.dev/io
参考にした情報源リンク
- Go言語の公式ドキュメント
- Gitの差分表示
- Go言語のソースコードリポジトリ (GitHub)