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

[インデックス 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.Readerio.Writer インターフェースを実装しているため、I/O操作で非常に頻繁に利用されます。

コミットメッセージが示唆するように、bytes.NewBuffer(nil) という形式での初期化が一部で慣習化されていましたが、これは bytes.Buffer の設計意図やGoのゼロ値の概念からすると、必ずしも最適ではありませんでした。

bytes.NewBuffer(nil) は、内部的に bytes.Bufferbuf フィールド(バイトスライス)を 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.Readerio.Writer の実装: Read メソッドと Write メソッドを持つため、io.Copy などの汎用的なI/O関数と組み合わせて使用できます。これにより、バイトデータをメモリ上で効率的に操作したり、他のI/Oストリームとの橋渡しをしたりするのに非常に便利です。
  • 文字列変換: String() メソッドでバッファの内容を文字列として取得できます。
  • リセット可能: Reset() メソッドでバッファをクリアし、再利用できます。

bytes.Buffer の初期化方法

Goでは、構造体の初期化にはいくつかの方法があります。

  1. ゼロ値による初期化:

    var buf bytes.Buffer
    

    これは bytes.Buffer 型の変数を宣言する最もシンプルな方法です。Goの仕様により、構造体のフィールドはそれぞれの型のゼロ値で初期化されます。bytes.Buffer の場合、内部のバイトスライスは nil に、その他のフィールドもゼロ値になります。この状態の buf はすぐに Write メソッドなどで使用できます。

  2. new キーワードによる初期化:

    buf := new(bytes.Buffer)
    

    new キーワードは、指定された型の新しいインスタンスへのポインタを返します。この場合も、bytes.Buffer のフィールドはゼロ値で初期化されます。buf*bytes.Buffer 型のポインタになります。

  3. 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 と宣言した場合、bufbytes.Buffer 型の構造体としてスタック上に確保され、そのフィールドはゼロ値で初期化されます。bytes.Bufferbuf フィールド([]byte 型)のゼロ値は nil スライスです。したがって、var buf bytes.Bufferbytes.Buffer{buf: nil} と同等になります。

また、new(bytes.Buffer)*bytes.Buffer 型のポインタを返しますが、これも指し示す bytes.Buffer 構造体のフィールドはゼロ値で初期化されるため、&bytes.Buffer{buf: nil} と同等です。

つまり、bytes.NewBuffer(nil)var buf bytes.Buffernew(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つです。

  1. 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)

  2. 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)

  3. 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言語の公式ドキュメント
  • 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 (この変更の背景にある議論)