[インデックス 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.NewReader
と strings.NewReader
は、それぞれ既存の []byte
と string
を io.Reader
インターフェースとして扱うための、より軽量で読み取り専用の構造体を提供します。これらの型は、元のデータへの参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。
したがって、このコミットは、データが既にメモリ上に存在し、それを読み取り専用として扱う場合に、より適切な Reader
実装を選択することで、Go標準ライブラリ全体のメモリ効率とパフォーマンスを向上させることを目的としています。特に、テストコードやユーティリティ関数など、一時的なデータソースとして bytes.Buffer
が使われていた箇所で、この最適化が適用されています。
前提知識の解説
このコミットを理解するためには、Go言語の以下の概念を理解しておく必要があります。
-
io.Reader
インターフェース:io.Reader
はGo言語の標準ライブラリio
パッケージで定義されているインターフェースで、データを読み出すための単一のメソッドRead(p []byte) (n int, err error)
を持ちます。このインターフェースを実装する型は、データソースからバイトを読み取り、提供されたバイトスライスp
に書き込みます。n
は読み取られたバイト数、err
はエラー(EOFを含む)を示します。Goでは、ファイル、ネットワーク接続、メモリ上のデータなど、様々なデータソースがこのio.Reader
インターフェースを実装しており、統一的な方法でデータを扱うことができます。 -
bytes.Buffer
:bytes
パッケージのBuffer
型は、可変長のバイトバッファを実装します。これはio.Reader
とio.Writer
の両方のインターフェースを実装しており、データの書き込み(Write
メソッドなど)と読み出し(Read
メソッドなど)の両方が可能です。- 特徴: データを内部のバイトスライスに保持し、必要に応じてその容量を自動的に拡張します。
- 用途: データを incrementally に構築し、その後読み出すようなシナリオ(例: HTTPレスポンスボディの構築、ログメッセージの収集)に非常に適しています。
- メモリ:
bytes.Buffer
は内部でバイトスライスを管理し、書き込み操作によってそのサイズが大きくなる可能性があります。初期化時にbytes.NewBuffer([]byte("data"))
やbytes.NewBufferString("data")
のように既存のデータで初期化する場合、そのデータは内部バッファにコピーされます。
-
bytes.NewReader
:bytes
パッケージのNewReader
関数は、既存のバイトスライス ([]byte
) からデータを読み出すためのio.Reader
を返します。- 特徴:
io.Reader
,io.Seeker
,io.ReaderAt
インターフェースを実装します。 - 用途: 既にメモリ上に存在する
[]byte
データを読み取り専用のストリームとして扱いたい場合に最適です。 - メモリ:
bytes.NewReader
は、引数として渡された[]byte
への参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。これにより、メモリ効率が非常に高くなります。
- 特徴:
-
strings.NewReader
:strings
パッケージのNewReader
関数は、既存の文字列 (string
) からデータを読み出すためのio.Reader
を返します。- 特徴:
io.Reader
,io.Seeker
,io.ReaderAt
インターフェースを実装します。 - 用途: 既にメモリ上に存在する
string
データを読み取り専用のストリームとして扱いたい場合に最適です。 - メモリ:
strings.NewReader
は、引数として渡されたstring
への参照を保持するだけで、新しいメモリを割り当てたり、データをコピーしたりすることはありません。Goの文字列は不変であるため、この参照は安全です。
- 特徴:
bytes.Buffer
と bytes.NewReader
/strings.NewReader
の違い:
主な違いは、bytes.Buffer
が読み書き両用で内部バッファを動的に管理するのに対し、bytes.NewReader
と strings.NewReader
は読み取り専用であり、既存のデータへの参照を保持するだけである点です。データが既に確定しており、変更されることがなく、単に読み取りたいだけであれば、bytes.NewReader
や strings.NewReader
を使用する方が、メモリのコピーや不要なバッファ拡張を避けることができるため、より効率的です。
技術的詳細
このコミットで行われている技術的な変更は、Go言語の標準ライブラリ内の多くのテストファイルやユーティリティ関数において、データソースの初期化方法を最適化することです。具体的には、以下のパターンで変更が行われています。
bytes.NewBuffer([]byte(data))
の代わりにbytes.NewReader([]byte(data))
を使用。bytes.NewBufferString(data)
の代わりにstrings.NewReader(data)
を使用。
この変更の技術的なメリットは以下の通りです。
-
メモリ効率の向上:
bytes.Buffer
を既存のデータで初期化する場合(例:bytes.NewBuffer([]byte("some data"))
)、初期化時にそのデータがbytes.Buffer
の内部バッファにコピーされます。これは、元のデータが既にメモリに存在しているにもかかわらず、そのデータの複製を作成することを意味します。- 一方、
bytes.NewReader([]byte("some data"))
やstrings.NewReader("some data")
は、元のバイトスライスや文字列への参照を保持するだけで、データのコピーは行いません。これにより、メモリ使用量を削減し、ガベージコレクションの負荷を軽減できます。
-
パフォーマンスの向上:
- データのコピーが不要になるため、オブジェクトの生成コストが低減されます。特に、頻繁に小さなデータソースを作成して読み取るようなシナリオでは、このオーバーヘッドの削減が全体的なパフォーマンスに寄与します。
bytes.Buffer
は書き込み操作のために内部バッファを動的に拡張するロジックを持っていますが、読み取り専用の用途ではこの機能は不要であり、そのための内部的な複雑さやオーバーヘッドも回避できます。
-
意図の明確化: コードを読む人にとって、
bytes.NewReader
やstrings.NewReader
を使用していることは、そのデータソースが読み取り専用であり、変更されないことが明確に伝わります。これはコードの可読性と保守性を向上させます。
この変更は、Goの標準ライブラリ全体にわたる広範なリファクタリングであり、特にテストコードで多く見られます。テストでは、特定の入力データに対して関数やメソッドの動作を検証するために、一時的なデータソースを頻繁に作成します。このような場面で bytes.NewReader
や strings.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
として扱うことができ、より自然で効率的です。
これらの変更は、compress
、crypto
、debug
、encoding
、mime
、net/http
、runtime
、text
など、Go標準ライブラリの様々なパッケージのテストファイルや内部実装にわたって行われています。
コアとなるコードの解説
このコミットのコアとなる変更は、bytes.Buffer
のインスタンスを bytes.NewReader
または strings.NewReader
のインスタンスに置き換えるという、シンプルながらも広範なパターンに基づいています。
変更の論理:
-
bytes.Buffer
の役割の再評価:bytes.Buffer
は、データを動的に構築し、その後読み出すという「書き込みと読み出し」の両方の機能が必要な場合に最適です。しかし、多くのケース、特にテストコードや特定のユーティリティ関数では、既に存在する固定のバイトデータや文字列をio.Reader
として扱うだけで十分です。このような場合、bytes.Buffer
を使用すると、不要なメモリ割り当て(元のデータのコピー)や、動的なバッファ拡張のための内部ロジックのオーバーヘッドが発生します。 -
bytes.NewReader
とstrings.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.NewReader
は raw
スライスへのポインタと長さを保持するだけで、データのコピーは行いません。これにより、メモリ使用量が削減され、オブジェクトの生成が高速化されます。
同様に、reader := bytes.NewBufferString(data)
は data
文字列の内容を bytes.Buffer
の内部バッファにコピーしますが、reader := strings.NewReader(data)
は data
文字列への参照を保持するだけです。
この変更は、Goの標準ライブラリ全体にわたる多数のファイルに適用されており、特にテストコードで顕著です。テストでは、特定の入力データ(しばしば固定された文字列やバイトスライス)を io.Reader
として提供する必要があるため、これらの軽量な Reader
実装が非常に適しています。
結果として、このコミットは、Go標準ライブラリの全体的な効率性、特にメモリフットプリントとガベージコレクションのパフォーマンスを向上させることに貢献しています。これは、Goの設計哲学である「シンプルさ」と「効率性」に合致する変更と言えます。
関連リンク
- Go CL 54660045: https://golang.org/cl/54660045
- GitHub Commit: https://github.com/golang/go/commit/a18f4ab56942f996607c08be56060a892b65822d
参考にした情報源リンク
- Go
bytes
パッケージドキュメント: https://pkg.go.dev/bytes - Go
strings
パッケージドキュメント: https://pkg.go.dev/strings - Go
io
パッケージドキュメント: https://pkg.go.dev/io bytes.Buffer
vsbytes.NewReader
vsstrings.NewReader
のパフォーマンスとメモリに関する議論 (Stack Overflow, Reddit, ブログ記事など):- https://stackoverflow.com/questions/28322077/bytes-newbuffer-vs-bytes-newreader-in-go
- https://www.organicprogrammer.com/posts/go-io-reader-writer-buffer-builder/
- https://www.ravelin.com/blog/go-bytes-buffer-vs-strings-builder
- https://scalent.io/blog/go-io-reader-writer-buffer-builder/
- https://yuxuan.org/posts/2020/03/20/go-io-reader-writer-buffer-builder/
- https://golangnote.com/topics/100
- https://www.reddit.com/r/golang/comments/102120/bytesbuffer_vs_bytesnewreader/
- https://www.reddit.com/r/golang/comments/102120/bytesbuffer_vs_bytesnewreader/
- https://stackoverflow.com/questions/28322077/bytes-newbuffer-vs-bytes-newreader-in-go
- https://go.dev/blog/strings
- https://www.organicprogrammer.com/posts/go-io-reader-writer-buffer-builder/
- https://www.ravelin.com/blog/go-bytes-buffer-vs-strings-builder
- https://golangnote.com/topics/100
- https://stackoverflow.com/questions/28322077/bytes-newbuffer-vs-bytes-newreader-in-go
- https://scalent.io/blog/go-io-reader-writer-buffer-builder/
- https://yuxuan.org/posts/2020/03/20/go-io-reader-writer-buffer-builder/
- https://go.dev/pkg/strings/#NewReader
- https://www.reddit.com/r/golang/comments/102120/bytesbuffer_vs_bytesnewreader/
- https://stackoverflow.com/questions/28322077/bytes-newbuffer-vs-bytes-newreader-in-go