[インデックス 11637] ファイルの概要
このコミットは、Go言語の標準ライブラリ全体で bytes.NewBuffer(nil)
の使用を new(bytes.Buffer)
または var buf bytes.Buffer
に置き換える変更を加えています。これは、bytes.NewBuffer(nil)
が bytes.Buffer
を初期化する推奨される方法ではないという認識を広め、より慣用的で効率的な初期化方法を促進することを目的としています。特に html/token.go
のような一部のファイルでは、この慣用的な初期化のポイントが完全に理解されていなかったことが示唆されています。
コミット
commit 5be24046c7b40d0ed522cba8d38c45e406269b28
Author: Rob Pike <r@golang.org>
Date: Mon Feb 6 14:09:00 2012 +1100
all: avoid bytes.NewBuffer(nil)
The practice encourages people to think this is the way to
create a bytes.Buffer when new(bytes.Buffer) or
just var buf bytes.Buffer work fine.
(html/token.go was missing the point altogether.)
R=golang-dev, bradfitz, r
CC=golang-dev
https://golang.org/cl/5637043
GitHub上でのコミットページへのリンク
https://github.com/golang/go/commit/5be24046c7b40d0ed522cba8d38c45e406269b28
元コミット内容
all: avoid bytes.NewBuffer(nil)
The practice encourages people to think this is the way to
create a bytes.Buffer when new(bytes.Buffer) or
just var buf bytes.Buffer work fine.
(html/token.go was missing the point altogether.)
変更の背景
この変更の背景には、Go言語の bytes.Buffer
型の初期化に関するベストプラクティスの確立があります。bytes.Buffer
は可変長のバイトシーケンスを扱うための型で、io.Reader
や io.Writer
インターフェースを実装しているため、I/O操作で非常に頻繁に利用されます。
コミットメッセージが示唆するように、bytes.NewBuffer(nil)
という形式での初期化が一部で慣習化されていましたが、これは bytes.Buffer
の設計意図やGoのゼロ値の概念からすると、必ずしも最適ではありませんでした。
bytes.NewBuffer(nil)
は、内部的に bytes.Buffer
の buf
フィールド(バイトスライス)を nil
で初期化します。しかし、bytes.Buffer
のゼロ値(var buf bytes.Buffer
で宣言した場合)は、buf
フィールドが nil
の状態で、かつ cap
が0の状態で初期化されます。どちらの方法でも、初めて書き込みが行われる際に内部バッファが適切に割り当てられるため、機能的には問題ありません。
しかし、bytes.NewBuffer(nil)
は関数呼び出しを伴うため、new(bytes.Buffer)
や var buf bytes.Buffer
と比較して、わずかながらオーバーヘッドが発生します。また、NewBuffer
という名前から、あたかも bytes.Buffer
を作成する唯一の、あるいは最も適切な方法であるかのような誤解を招く可能性がありました。
このコミットは、Goの設計哲学である「シンプルさ」と「慣用的なコード」を追求し、開発者がより効率的で読みやすいコードを書くことを奨励するために行われました。特に、bytes.Buffer
のゼロ値がすぐに使える状態であるというGoの重要な特性を強調しています。
前提知識の解説
Go言語の bytes.Buffer
bytes.Buffer
は、Go言語の標準ライブラリ bytes
パッケージで提供される型です。その名の通り、バイトのバッファ(一時的な記憶領域)として機能します。主な特徴は以下の通りです。
- 可変長: 必要に応じて内部のバイトスライスが自動的に拡張されます。
io.Reader
とio.Writer
の実装:Read
メソッドとWrite
メソッドを持つため、io.Copy
などの汎用的なI/O関数と組み合わせて使用できます。これにより、バイトデータをメモリ上で効率的に操作したり、他のI/Oストリームとの橋渡しをしたりするのに非常に便利です。- 文字列変換:
String()
メソッドでバッファの内容を文字列として取得できます。 - リセット可能:
Reset()
メソッドでバッファをクリアし、再利用できます。
bytes.Buffer
の初期化方法
Goでは、構造体の初期化にはいくつかの方法があります。
-
ゼロ値による初期化:
var buf bytes.Buffer
これは
bytes.Buffer
型の変数を宣言する最もシンプルな方法です。Goの仕様により、構造体のフィールドはそれぞれの型のゼロ値で初期化されます。bytes.Buffer
の場合、内部のバイトスライスはnil
に、その他のフィールドもゼロ値になります。この状態のbuf
はすぐにWrite
メソッドなどで使用できます。 -
new
キーワードによる初期化:buf := new(bytes.Buffer)
new
キーワードは、指定された型の新しいインスタンスへのポインタを返します。この場合も、bytes.Buffer
のフィールドはゼロ値で初期化されます。buf
は*bytes.Buffer
型のポインタになります。 -
bytes.NewBuffer
関数による初期化:buf := bytes.NewBuffer([]byte("initial data")) // 初期データを与える場合 buf := bytes.NewBuffer(nil) // nil を与える場合
bytes.NewBuffer
関数は、バイトスライスを引数に取り、その内容で初期化された*bytes.Buffer
を返します。引数にnil
を渡すことも可能で、この場合は空のバッファが作成されます。
Go言語のゼロ値
Go言語の重要な概念の一つに「ゼロ値 (zero value)」があります。変数を宣言した際に明示的に初期化しなくても、その型に応じたデフォルト値(ゼロ値)が自動的に割り当てられます。
- 数値型 (int, float64など):
0
- ブール型 (bool):
false
- 文字列型 (string):
""
(空文字列) - ポインタ、スライス、マップ、チャネル、インターフェース:
nil
- 構造体: 各フィールドがそれぞれのゼロ値で初期化されます。
Goでは、ゼロ値が常に有効な状態であり、すぐに使えるように設計されています。これは、他の言語でよく見られる「nullポインタ例外」のような問題を回避するのに役立ちます。bytes.Buffer
の場合も、ゼロ値 (var buf bytes.Buffer
) で宣言されたインスタンスは、追加の初期化なしに Write
メソッドなどを呼び出すことができます。
技術的詳細
このコミットの技術的な核心は、bytes.Buffer
の初期化における冗長性と非効率性の排除です。
bytes.NewBuffer(nil)
の呼び出しは、bytes.NewBuffer
関数が内部で new(bytes.Buffer)
を呼び出し、そのポインタを返すという処理を含みます。具体的には、bytes.NewBuffer
の実装は以下のようになっています(Go 1.0.x 時代のコードを想定):
func NewBuffer(buf []byte) *Buffer {
return &Buffer{buf: buf}
}
bytes.NewBuffer(nil)
とすると、&Buffer{buf: nil}
が返されます。
一方、var buf bytes.Buffer
と宣言した場合、buf
は bytes.Buffer
型の構造体としてスタック上に確保され、そのフィールドはゼロ値で初期化されます。bytes.Buffer
の buf
フィールド([]byte
型)のゼロ値は nil
スライスです。したがって、var buf bytes.Buffer
は bytes.Buffer{buf: nil}
と同等になります。
また、new(bytes.Buffer)
は *bytes.Buffer
型のポインタを返しますが、これも指し示す bytes.Buffer
構造体のフィールドはゼロ値で初期化されるため、&bytes.Buffer{buf: nil}
と同等です。
つまり、bytes.NewBuffer(nil)
、var buf bytes.Buffer
、new(bytes.Buffer)
のいずれも、結果として buf
フィールドが nil
の空の bytes.Buffer
を作成します。しかし、bytes.NewBuffer(nil)
は関数呼び出しのオーバーヘッドを伴うため、わずかながら非効率です。また、NewBuffer
という関数名が、初期データがない場合でも「バッファを作成する」ための唯一の手段であるかのような誤解を招く可能性がありました。
このコミットは、このような冗長な関数呼び出しを排除し、より直接的で効率的な初期化方法 (var buf bytes.Buffer
または new(bytes.Buffer)
) を採用することで、コードの意図を明確にし、Goのゼロ値の概念をより適切に活用することを目的としています。
特に html/token.go
の変更点 buf := bytes.NewBufferString(t.Data)
は注目に値します。これは bytes.NewBuffer(nil)
を bytes.NewBufferString
に変更しており、これは bytes.NewBuffer
の引数に []byte(t.Data)
を渡すのと同等です。この変更は、単に nil
初期化を避けるだけでなく、初期データがある場合には bytes.NewBuffer
を適切に利用するという意図も示しています。
コアとなるコードの変更箇所
このコミットでは、Go標準ライブラリ内の多数のファイルで bytes.NewBuffer(nil)
のパターンが修正されています。主な変更パターンは以下の2つです。
-
bytes.NewBuffer(nil)
をnew(bytes.Buffer)
に変更: これは、bytes.Buffer
のポインタが必要な場合(例えば、関数が*bytes.Buffer
を引数として取る場合)に適用されます。 例:- l, _ := NewReaderSize(bytes.NewBuffer(nil), minReadBufferSize) + l, _ := NewReaderSize(new(bytes.Buffer), minReadBufferSize)
(
src/pkg/bufio/bufio_test.go
) -
bytes.NewBuffer(nil)
をvar buf bytes.Buffer
に変更: これは、bytes.Buffer
の値型が必要な場合、またはポインタを渡す際にアドレス演算子&
を使用する場合に適用されます。 例:- buffer := bytes.NewBuffer(nil) - w := NewWriter(buffer, level) + var buffer bytes.Buffer + w := NewWriter(&buffer, level)
(
src/pkg/compress/flate/deflate_test.go
) -
bytes.NewBuffer(nil)
をbytes.NewBufferString(s)
に変更: これは、bytes.Buffer
を文字列で初期化する場合に適用されます。 例:- buf := bytes.NewBuffer(nil) - buf.WriteString(t.Data) + buf := bytes.NewBufferString(t.Data)
(
src/pkg/exp/html/token.go
)
影響を受けたファイルは多岐にわたり、bufio
, compress
, encoding
, exp/html
, html
, image
, old/template
など、Go標準ライブラリの様々なパッケージにわたっています。これは、bytes.NewBuffer(nil)
の使用が広範にわたっていたことを示しています。
コアとなるコードの解説
変更の核心は、bytes.Buffer
の初期化における冗長な関数呼び出しを排除し、Goのゼロ値の概念をより適切に活用することにあります。
bytes.NewBuffer(nil)
から new(bytes.Buffer)
または var buf bytes.Buffer
へ
bytes.NewBuffer(nil)
: この形式は、bytes.NewBuffer
関数を呼び出し、その引数にnil
を渡します。bytes.NewBuffer
は内部で新しいbytes.Buffer
のインスタンスをヒープに割り当て、そのポインタを返します。この際、内部バッファはnil
スライスとして初期化されます。new(bytes.Buffer)
:new
キーワードは、指定された型のゼロ値で初期化された新しいインスタンスをヒープに割り当て、そのポインタを返します。bytes.Buffer
のゼロ値は、内部バッファがnil
スライスである状態です。したがって、bytes.NewBuffer(nil)
と全く同じ状態の*bytes.Buffer
が得られますが、関数呼び出しのオーバーヘッドがありません。var buf bytes.Buffer
: これはbytes.Buffer
型の変数をスタック上に宣言し、そのゼロ値で初期化します。bytes.Buffer
のゼロ値は、内部バッファがnil
スライスである状態です。この場合、buf
は値型であり、ポインタが必要な場合は&buf
のようにアドレス演算子を使用します。スタック割り当てはヒープ割り当てよりも一般的に高速です。
これらの変更により、コードはより直接的になり、bytes.Buffer
のゼロ値がすぐに使えるというGoの特性が強調されます。機能的な違いはほとんどありませんが、パフォーマンスのわずかな向上と、コードの意図の明確化が図られます。
bytes.NewBuffer(nil)
から bytes.NewBufferString(s)
へ
src/pkg/exp/html/token.go
の以下の変更は特に重要です。
- buf := bytes.NewBuffer(nil)
- buf.WriteString(t.Data)
+ buf := bytes.NewBufferString(t.Data)
元のコードでは、まず空のバッファを作成し、その後に WriteString
メソッドでデータを書き込んでいました。新しいコードでは、bytes.NewBufferString
関数を使用することで、バッファの作成と初期データの書き込みを1ステップで行っています。
bytes.NewBufferString(s)
は、内部的に bytes.NewBuffer([]byte(s))
と同様の動作をします。これにより、初期データが既にある場合に、より効率的かつ簡潔にバッファを初期化できます。これは、bytes.NewBuffer
が初期データを持つバッファを作成するための関数であるという本来の意図に沿った使用方法です。
これらの変更は、Goの標準ライブラリ全体でコードの品質と慣用性を向上させるための継続的な取り組みの一環として行われました。
関連リンク
- Go言語
bytes
パッケージのドキュメント: https://pkg.go.dev/bytes - Go言語
io
パッケージのドキュメント: https://pkg.go.dev/io - Go言語のゼロ値に関する解説 (Go公式ブログ): https://go.dev/blog/go-zero-values
参考にした情報源リンク
- Go言語の公式ドキュメント
- Go言語のソースコード (特に
bytes
パッケージ) - Go言語のゼロ値に関する一般的な解説記事
- Go言語の
bytes.Buffer
の使用方法に関する一般的な解説記事 - GitHubのコミット履歴と関連する議論 (golang/goリポジトリ)
- Go Code Review Comments: https://go.dev/doc/effective_go#zero_value (ゼロ値に関するGoの慣習)
- Go CL 5637043: https://go.dev/cl/5637043 (このコミットに対応するGoの変更リスト)
- Go issue 2900: bytes.NewBuffer(nil) is not idiomatic: https://go.dev/issue/2900 (この変更の背景にある議論)