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

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

このコミットは、Go言語の標準ライブラリbytesパッケージ内のJoin関数およびReplace関数における潜在的なバグを修正するものです。具体的には、特定の条件下でこれらの関数が新しいバッファを返さず、入力スライスと同じ基盤配列を共有してしまう(エイリアシング問題)ことで、予期せぬ副作用を引き起こす可能性があった問題を解決します。

コミット

commit c0efcac6a97588f7013b7ec09dd56cb780bdce64
Author: Gustavo Niemeyer <gustavo@niemeyer.net>
Date:   Fri Jul 20 16:04:22 2012 -0300

    bytes: make Join return a new buffer on len(a) == 1
    
    Fixes #3844.
    
    R=golang-dev, r
    CC=golang-dev
    https://golang.org/cl/6432054

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

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

元コミット内容

このコミットは、bytesパッケージのJoin関数が、入力スライスaの長さが1である場合に、新しいバッファを返すように変更することを目的としています。これにより、Join関数が返す結果が入力スライスとメモリを共有する(エイリアシングする)ことによって発生する可能性のあるバグ(Issue #3844)を修正します。また、Replace関数についても同様のエイリアシング問題が修正されています。

変更の背景

この変更の背景には、Go言語のスライスが基盤となる配列への参照であるという特性があります。bytes.Join関数は、複数のバイトスライスを結合して1つのバイトスライスを生成します。しかし、入力スライスaが1つの要素しか持たない場合、以前の実装ではa[0]をそのまま返していました。これは、a[0]Joinの戻り値が同じ基盤配列を共有することを意味します。

このエイリアシングは、以下のような問題を引き起こす可能性がありました。

  1. 予期せぬデータ変更: Joinの戻り値が変更された場合、元の入力スライスa[0]も意図せず変更されてしまう。これは、関数が「新しい」結合されたデータを提供すると期待するユーザーにとって、非常に混乱を招く動作です。
  2. 不変性の期待違反: 多くのプログラミング言語において、文字列やバイト列の結合操作は新しいメモリ領域に結果を生成し、元のデータは変更しないという不変性の原則に従います。このエイリアシングは、その期待に反するものでした。

同様の問題はbytes.Replace関数にも存在しました。Replace関数は、スライス内の特定のバイト列を別のバイト列に置換しますが、置換が発生しない場合(m == 0)、以前の実装では入力スライスsのコピーを返さず、sをそのまま返していました。これもまた、エイリアシングによる予期せぬ変更のリスクをはらんでいました。

これらの問題は、GoのIssue #3844として報告され、このコミットによって修正されました。

前提知識の解説

Go言語のスライス (Slice)

Go言語のスライスは、配列の一部を参照する軽量なデータ構造です。スライスは以下の3つの要素で構成されます。

  • ポインタ (Pointer): スライスが参照する基盤配列の先頭要素へのポインタ。
  • 長さ (Length): スライスに含まれる要素の数。
  • 容量 (Capacity): スライスの先頭要素から基盤配列の末尾までの要素の数。

スライスは、基盤となる配列のビュー(view)として機能します。そのため、複数のスライスが同じ基盤配列の一部または全体を参照することが可能です。これを「エイリアシング (Aliasing)」と呼びます。エイリアシングが発生すると、あるスライスを介して基盤配列の要素を変更すると、同じ基盤配列を参照している他のスライスからもその変更が見えることになります。

bytes.Join 関数

bytes.Join関数は、[][]byte型のスライス(バイトスライスのスライス)を受け取り、それらを指定されたセパレータバイトスライスで結合して、1つの[]byte型のスライスを返します。

例:

s := [][]byte{[]byte("foo"), []byte("bar"), []byte("baz")}
sep := []byte("-")
result := bytes.Join(s, sep) // result は []byte("foo-bar-baz")

bytes.Replace 関数

bytes.Replace関数は、[]byte型のスライスs、置換対象のoldバイトスライス、置換後のnewバイトスライス、および置換回数を指定するnを受け取ります。s内のoldの出現箇所をnewで置換した新しいバイトスライスを返します。nが負の値の場合、すべての出現箇所が置換されます。

例:

s := []byte("oink oink oink")
old := []byte("oink")
new := []byte("moo")
result := bytes.Replace(s, old, new, 2) // result は []byte("moo moo oink")

append([]byte(nil), ...) のイディオム

Go言語で新しいスライスを作成し、既存のスライスの内容をコピーする一般的なイディオムとして、append([]byte(nil), originalSlice...) があります。

  • []byte(nil): これはnilスライスを作成します。nilスライスは長さも容量も0ですが、有効なスライスです。
  • append(...): append関数は、最初の引数にスライス、その後に可変長引数として追加する要素を受け取ります。
  • originalSlice...: これは「スライスを展開する」構文で、originalSliceの各要素を個別の引数としてappend関数に渡します。

この組み合わせにより、originalSliceのすべての要素が新しい基盤配列を持つnilスライスに追加され、結果としてoriginalSliceの完全なコピーである新しいスライスが返されます。この新しいスライスは、元のスライスとは異なるメモリ領域を指すため、エイリアシングの問題を回避できます。

技術的詳細

このコミットは、bytes.Joinbytes.Replaceにおけるエイリアシングの問題を解決するために、特定の条件下で明示的に新しいバッファを割り当ててコピーを行うように変更しています。

bytes.Join の修正

以前のbytes.Joinの実装では、入力スライスaの長さが1の場合、return a[0]としていました。これは、a[0]が参照する基盤配列をそのまま返り値が共有することを意味します。

修正後:

func Join(a [][]byte, sep []byte) []byte {
	if len(a) == 0 {
		return []byte{}
	}
	if len(a) == 1 {
		// Just return a copy.
		return append([]byte(nil), a[0]...)
	}
	// ... (既存の結合ロジック)
}

len(a) == 1の場合に、append([]byte(nil), a[0]...)を使用することで、a[0]の内容を新しいバイトスライスにコピーして返します。これにより、Joinの呼び出し元が返されたスライスを変更しても、元のa[0]の内容には影響が及ばなくなります。

bytes.Replace の修正

以前のbytes.Replaceの実装では、置換対象のoldが見つからない場合(m == 0)、sのコピーを作成せずにsをそのまま返していました。

修正後:

func Replace(s, old, new []byte, n int) []byte {
	// ... (既存のロジック)
	if m == 0 {
		// Just return a copy.
		return append([]byte(nil), s...)
	}
	// ... (既存の置換ロジック)
}

m == 0の場合に、append([]byte(nil), s...)を使用することで、入力スライスsの内容を新しいバイトスライスにコピーして返します。これにより、Replaceの呼び出し元が返されたスライスを変更しても、元のsの内容には影響が及ばなくなります。

これらの変更により、bytes.Joinbytes.Replaceは、入力スライスが特定の条件を満たす場合でも、常に新しい独立したバイトスライスを返すようになり、エイリアシングによる予期せぬ副作用が排除されました。

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

--- a/src/pkg/bytes/bytes.go
+++ b/src/pkg/bytes/bytes.go
@@ -333,14 +333,15 @@ func FieldsFunc(s []byte, f func(rune) bool) [][]byte {
 	return a[0:na]
 }
 
-// Join concatenates the elements of a to create a single byte array.   The separator
+// Join concatenates the elements of a to create a new byte array. The separator
 // sep is placed between elements in the resulting array.
 func Join(a [][]byte, sep []byte) []byte {
 	if len(a) == 0 {
 		return []byte{}
 	}
 	if len(a) == 1 {
-		return a[0]
+		// Just return a copy.
+		return append([]byte(nil), a[0]...)
 	}
 	n := len(sep) * (len(a) - 1)
 	for i := 0; i < len(a); i++ {
@@ -619,10 +620,8 @@ func Replace(s, old, new []byte, n int) []byte {
 		m = Count(s, old)
 	}
 	if m == 0 {
-		// Nothing to do. Just copy.
-		t := make([]byte, len(s))
-		copy(t, s)
-		return t
+		// Just return a copy.
+		return append([]byte(nil), s...)
 	}
 	if n < 0 || m < n {
 		n = m
diff --git a/src/pkg/bytes/bytes_test.go b/src/pkg/bytes/bytes_test.go
index 000f235176..0e2ef504cf 100644
--- a/src/pkg/bytes/bytes_test.go
+++ b/src/pkg/bytes/bytes_test.go
@@ -490,6 +490,12 @@ func TestSplit(t *testing.T) {
 			t.Errorf("Split disagrees withSplitN(%q, %q, %d) = %v; want %v", tt.s, tt.sep, tt.n, b, a)
 			}
 		}
+		if len(a) > 0 {
+			in, out := a[0], s
+			if cap(in) == cap(out) && &in[:1][0] == &out[:1][0] {
+				t.Errorf("Join(%#v, %q) didn't copy", a, tt.sep)
+			}
+		}
 	}
 }
 

コアとなるコードの解説

src/pkg/bytes/bytes.go

  • Join関数の変更:

    • 変更前: if len(a) == 1 { return a[0] }
    • 変更後: if len(a) == 1 { // Just return a copy. return append([]byte(nil), a[0]...) }
    • この変更により、入力スライスaが単一の要素しか持たない場合でも、a[0]の基盤配列を直接返すのではなく、a[0]の内容を新しいバッファにコピーして返します。append([]byte(nil), a[0]...)は、a[0]の要素を新しいnilスライスに追加することで、効率的に新しい独立したスライスを作成するGoのイディオムです。
  • Replace関数の変更:

    • 変更前:
      if m == 0 {
          // Nothing to do. Just copy.
          t := make([]byte, len(s))
          copy(t, s)
          return t
      }
      
    • 変更後: if m == 0 { // Just return a copy. return append([]byte(nil), s...) }
    • この変更は、Replace関数が置換を行わない場合(m == 0)、以前はmakecopyを使って明示的にコピーを作成していた部分を、より簡潔で一般的なappend([]byte(nil), s...)のイディオムに置き換えています。機能的には同じく新しいバッファへのコピーを行いますが、コードがよりGoらしい書き方になっています。

src/pkg/bytes/bytes_test.go

  • TestSplit関数内の新しいテストケース:
    • Join関数のエイリアシング問題を検出するために、TestSplit関数(実際にはJoinのテストも兼ねている)内に新しいテストロジックが追加されました。
    • if len(a) > 0 { ... } のブロックが追加され、Joinの入力スライスaが空でない場合にテストを実行します。
    • in, out := a[0], s は、Joinの入力の最初の要素と、Joinの出力(sはテストケースの期待値であり、Joinの戻り値と比較される)をそれぞれinoutに代入しています。
    • if cap(in) == cap(out) && &in[:1][0] == &out[:1][0] { ... } がエイリアシングをチェックする核心部分です。
      • cap(in) == cap(out): 入力スライスと出力スライスの容量が同じであるかを確認します。これは、同じ基盤配列を共有している可能性を示唆します。
      • &in[:1][0] == &out[:1][0]: これは、inスライスの最初の要素のアドレスとoutスライスの最初の要素のアドレスが同じであるかを確認します。アドレスが同じであれば、両方のスライスが同じ基盤配列の同じ開始位置を指している、つまりエイリアシングしていることを意味します。
      • もしこの条件が真であれば、t.Errorf("Join(%#v, %q) didn't copy", a, tt.sep) が呼び出され、Joinがコピーを作成しなかったことをエラーとして報告します。

この新しいテストケースは、Join関数がlen(a) == 1の場合に適切に新しいバッファを返すことを保証するための重要な検証ステップです。

関連リンク

参考にした情報源リンク

  • Go Issue #3844 (関連する可能性のある情報): https://golang.org/issue/3844 (Web検索結果より、このコミットが修正したIssue #3844はbytes.Joinのエイリアシング問題に関連していることが示唆されています。)