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

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

このコミットは、Go言語の標準ライブラリ全体で、読み取り専用のデータソースとして bytes.Buffer の代わりに bytes.NewReader および strings.NewReader を使用するように変更するものです。これにより、メモリ効率の向上と、不要なメモリ割り当ての削減が図られています。

コミット

commit a18f4ab56942f996607c08be56060a892b65822d
Author: Brad Fitzpatrick <bradfitz@golang.org>
Date:   Mon Jan 27 11:05:01 2014 -0800

    all: use {bytes,strings}.NewReader instead of bytes.Buffers
    
    Use the smaller read-only bytes.NewReader/strings.NewReader instead
    of a bytes.Buffer when possible.
    
    LGTM=r
    R=golang-codereviews, r
    CC=golang-codereviews
    https://golang.org/cl/54660045

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

https://github.com/golang/go/commit/a18f4ab56942f996607c08be56060a892b65822d

元コミット内容

all: use {bytes,strings}.NewReader instead of bytes.Buffers

可能な場合、bytes.Buffer の代わりに、より小さく読み取り専用の bytes.NewReader/strings.NewReader を使用する。

変更の背景

この変更の背景には、Go言語におけるメモリ管理とI/O操作の最適化があります。bytes.Buffer は、バイトスライスを効率的に構築・書き込み、そして読み出すための可変長バッファとして設計されています。しかし、既に存在するバイトスライス ([]byte) や文字列 (string) からデータを読み出すだけであれば、bytes.Buffer を使用することは過剰な機能であり、不要なメモリ割り当てやオーバーヘッドを発生させる可能性があります。

bytes.NewReaderstrings.NewReader は、それぞれ既存の []bytestringio.Reader インターフェースとして扱うための、より軽量で読み取り専用の構造体を提供します。これらの型は、元のデータへの参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。

したがって、このコミットは、データが既にメモリ上に存在し、それを読み取り専用として扱う場合に、より適切な Reader 実装を選択することで、Go標準ライブラリ全体のメモリ効率とパフォーマンスを向上させることを目的としています。特に、テストコードやユーティリティ関数など、一時的なデータソースとして bytes.Buffer が使われていた箇所で、この最適化が適用されています。

前提知識の解説

このコミットを理解するためには、Go言語の以下の概念を理解しておく必要があります。

  1. io.Reader インターフェース: io.Reader はGo言語の標準ライブラリ io パッケージで定義されているインターフェースで、データを読み出すための単一のメソッド Read(p []byte) (n int, err error) を持ちます。このインターフェースを実装する型は、データソースからバイトを読み取り、提供されたバイトスライス p に書き込みます。n は読み取られたバイト数、err はエラー(EOFを含む)を示します。Goでは、ファイル、ネットワーク接続、メモリ上のデータなど、様々なデータソースがこの io.Reader インターフェースを実装しており、統一的な方法でデータを扱うことができます。

  2. bytes.Buffer: bytes パッケージの Buffer 型は、可変長のバイトバッファを実装します。これは io.Readerio.Writer の両方のインターフェースを実装しており、データの書き込み(Write メソッドなど)と読み出し(Read メソッドなど)の両方が可能です。

    • 特徴: データを内部のバイトスライスに保持し、必要に応じてその容量を自動的に拡張します。
    • 用途: データを incrementally に構築し、その後読み出すようなシナリオ(例: HTTPレスポンスボディの構築、ログメッセージの収集)に非常に適しています。
    • メモリ: bytes.Buffer は内部でバイトスライスを管理し、書き込み操作によってそのサイズが大きくなる可能性があります。初期化時に bytes.NewBuffer([]byte("data"))bytes.NewBufferString("data") のように既存のデータで初期化する場合、そのデータは内部バッファにコピーされます。
  3. bytes.NewReader: bytes パッケージの NewReader 関数は、既存のバイトスライス ([]byte) からデータを読み出すための io.Reader を返します。

    • 特徴: io.Reader, io.Seeker, io.ReaderAt インターフェースを実装します。
    • 用途: 既にメモリ上に存在する []byte データを読み取り専用のストリームとして扱いたい場合に最適です。
    • メモリ: bytes.NewReader は、引数として渡された []byte への参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。これにより、メモリ効率が非常に高くなります。
  4. strings.NewReader: strings パッケージの NewReader 関数は、既存の文字列 (string) からデータを読み出すための io.Reader を返します。

    • 特徴: io.Reader, io.Seeker, io.ReaderAt インターフェースを実装します。
    • 用途: 既にメモリ上に存在する string データを読み取り専用のストリームとして扱いたい場合に最適です。
    • メモリ: strings.NewReader は、引数として渡された string への参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。Goの文字列は不変であるため、この参照は安全です。

bytes.Bufferbytes.NewReader/strings.NewReader の違い: 主な違いは、bytes.Buffer が読み書き両用で内部バッファを動的に管理するのに対し、bytes.NewReaderstrings.NewReader は読み取り専用であり、既存のデータへの参照を保持するだけである点です。データが既に確定しており、変更されることがなく、単に読み取りたいだけであれば、bytes.NewReaderstrings.NewReader を使用する方が、メモリのコピーや不要なバッファ拡張を避けることができるため、より効率的です。

技術的詳細

このコミットで行われている技術的な変更は、Go言語の標準ライブラリ内の多くのテストファイルやユーティリティ関数において、データソースの初期化方法を最適化することです。具体的には、以下のパターンで変更が行われています。

  • bytes.NewBuffer([]byte(data)) の代わりに bytes.NewReader([]byte(data)) を使用。
  • bytes.NewBufferString(data) の代わりに strings.NewReader(data) を使用。

この変更の技術的なメリットは以下の通りです。

  1. メモリ効率の向上:

    • bytes.Buffer を既存のデータで初期化する場合(例: bytes.NewBuffer([]byte("some data")))、初期化時にそのデータが bytes.Buffer の内部バッファにコピーされます。これは、元のデータが既にメモリに存在しているにもかかわらず、そのデータの複製を作成することを意味します。
    • 一方、bytes.NewReader([]byte("some data"))strings.NewReader("some data") は、元のバイトスライスや文字列への参照を保持するだけで、データのコピーは行いません。これにより、メモリ使用量を削減し、ガベージコレクションの負荷を軽減できます。
  2. パフォーマンスの向上:

    • データのコピーが不要になるため、オブジェクトの生成コストが低減されます。特に、頻繁に小さなデータソースを作成して読み取るようなシナリオでは、このオーバーヘッドの削減が全体的なパフォーマンスに寄与します。
    • bytes.Buffer は書き込み操作のために内部バッファを動的に拡張するロジックを持っていますが、読み取り専用の用途ではこの機能は不要であり、そのための内部的な複雑さやオーバーヘッドも回避できます。
  3. 意図の明確化: コードを読む人にとって、bytes.NewReaderstrings.NewReader を使用していることは、そのデータソースが読み取り専用であり、変更されないことが明確に伝わります。これはコードの可読性と保守性を向上させます。

この変更は、Goの標準ライブラリ全体にわたる広範なリファクタリングであり、特にテストコードで多く見られます。テストでは、特定の入力データに対して関数やメソッドの動作を検証するために、一時的なデータソースを頻繁に作成します。このような場面で bytes.NewReaderstrings.NewReader を使用することは、テストの実行効率を高める上で有効です。

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

このコミットは、Go標準ライブラリ内の多数のファイルにわたる広範な変更を含んでいます。主な変更パターンは、bytes.Buffer のインスタンス化を bytes.NewReader または strings.NewReader に置き換えることです。

以下に、代表的な変更例をいくつか示します。

src/pkg/archive/tar/reader_test.go

--- a/src/pkg/archive/tar/reader_test.go
+++ b/src/pkg/archive/tar/reader_test.go
@@ -321,7 +321,7 @@ func TestParsePAXHeader(t *testing.T) {
 		{"mtime", "mtime=1350244992.023960108", "30 mtime=1350244992.023960108\n"}}
 	for _, test := range paxTests {
 		key, expected, raw := test[0], test[1], test[2]
-		reader := bytes.NewBuffer([]byte(raw))
+		reader := bytes.NewReader([]byte(raw))
 		headers, err := parsePAX(reader)
 		if err != nil {
 			t.Errorf("Couldn't parse correctly formatted headers: %v", err)
@@ -337,7 +337,7 @@ func TestParsePAXHeader(t *testing.T) {
 			t.Error("Buffer wasn't consumed")
 		}
 	}
-	badHeader := bytes.NewBuffer([]byte("3 somelongkey="))
+	badHeader := bytes.NewReader([]byte("3 somelongkey="))
 	if _, err := parsePAX(badHeader); err != ErrHeader {
 		t.Fatal("Unexpected success when parsing bad header")
 	}

ここでは、bytes.NewBuffer([]byte(raw))bytes.NewReader([]byte(raw)) に変更されています。これは、raw というバイトスライスからデータを読み取るだけで、書き込み操作は行わないため、bytes.NewReader がより適切であると判断されたためです。

src/pkg/bufio/bufio_test.go

--- a/src/pkg/bufio/bufio_test.go
+++ b/src/pkg/bufio/bufio_test.go
@@ -65,12 +65,12 @@ func readBytes(buf *Reader) string {
 
 func TestReaderSimple(t *testing.T) {
 	data := "hello world"
-	b := NewReader(bytes.NewBufferString(data))
+	b := NewReader(strings.NewReader(data))
 	if s := readBytes(b); s != "hello world" {
 		t.Errorf("simple hello world test failed: got %q", s)
 	}
 
-	b = NewReader(newRot13Reader(bytes.NewBufferString(data)))
+	b = NewReader(newRot13Reader(strings.NewReader(data)))
 	if s := readBytes(b); s != "uryyb jbeyq" {
 		t.Errorf("rot13 hello world test failed: got %q", s)
 	}

ここでは、bytes.NewBufferString(data)strings.NewReader(data) に変更されています。data が文字列であるため、strings.NewReader が直接文字列を io.Reader として扱うことができ、より自然で効率的です。

これらの変更は、compresscryptodebugencodingmimenet/httpruntimetext など、Go標準ライブラリの様々なパッケージのテストファイルや内部実装にわたって行われています。

コアとなるコードの解説

このコミットのコアとなる変更は、bytes.Buffer のインスタンスを bytes.NewReader または strings.NewReader のインスタンスに置き換えるという、シンプルながらも広範なパターンに基づいています。

変更の論理:

  1. bytes.Buffer の役割の再評価: bytes.Buffer は、データを動的に構築し、その後読み出すという「書き込みと読み出し」の両方の機能が必要な場合に最適です。しかし、多くのケース、特にテストコードや特定のユーティリティ関数では、既に存在する固定のバイトデータや文字列を io.Reader として扱うだけで十分です。このような場合、bytes.Buffer を使用すると、不要なメモリ割り当て(元のデータのコピー)や、動的なバッファ拡張のための内部ロジックのオーバーヘッドが発生します。

  2. bytes.NewReaderstrings.NewReader の適切な利用:

    • bytes.NewReader([]byte(...)) は、既存の []byte スライスを読み取り専用の io.Reader としてラップします。これは元のスライスへの参照を保持するだけで、データのコピーは行いません。
    • strings.NewReader(string) は、既存の string を読み取り専用の io.Reader としてラップします。Goの文字列は不変であるため、これも安全に参照を保持するだけで済みます。

具体的なコードの変更と影響:

例えば、reader := bytes.NewBuffer([]byte(raw)) というコードがあった場合、これは raw の内容を新しい bytes.Buffer の内部バッファにコピーします。もし raw が大きなデータであれば、このコピー操作は時間とメモリを消費します。

これを reader := bytes.NewReader([]byte(raw)) に変更すると、bytes.NewReaderraw スライスへのポインタと長さを保持するだけで、データのコピーは行いません。これにより、メモリ使用量が削減され、オブジェクトの生成が高速化されます。

同様に、reader := bytes.NewBufferString(data)data 文字列の内容を bytes.Buffer の内部バッファにコピーしますが、reader := strings.NewReader(data)data 文字列への参照を保持するだけです。

この変更は、Goの標準ライブラリ全体にわたる多数のファイルに適用されており、特にテストコードで顕著です。テストでは、特定の入力データ(しばしば固定された文字列やバイトスライス)を io.Reader として提供する必要があるため、これらの軽量な Reader 実装が非常に適しています。

結果として、このコミットは、Go標準ライブラリの全体的な効率性、特にメモリフットプリントとガベージコレクションのパフォーマンスを向上させることに貢献しています。これは、Goの設計哲学である「シンプルさ」と「効率性」に合致する変更と言えます。

関連リンク

参考にした情報源リンク