[インデックス 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
の戻り値が同じ基盤配列を共有することを意味します。
このエイリアシングは、以下のような問題を引き起こす可能性がありました。
- 予期せぬデータ変更:
Join
の戻り値が変更された場合、元の入力スライスa[0]
も意図せず変更されてしまう。これは、関数が「新しい」結合されたデータを提供すると期待するユーザーにとって、非常に混乱を招く動作です。 - 不変性の期待違反: 多くのプログラミング言語において、文字列やバイト列の結合操作は新しいメモリ領域に結果を生成し、元のデータは変更しないという不変性の原則に従います。このエイリアシングは、その期待に反するものでした。
同様の問題は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.Join
とbytes.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.Join
とbytes.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
)、以前はmake
とcopy
を使って明示的にコピーを作成していた部分を、より簡潔で一般的な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
の戻り値と比較される)をそれぞれin
とout
に代入しています。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
の場合に適切に新しいバッファを返すことを保証するための重要な検証ステップです。
関連リンク
- GitHubコミットページ: https://github.com/golang/go/commit/c0efcac6a97588f7013b7ec09dd56cb780bdce64
- Go Code Review (CL): https://golang.org/cl/6432054
参考にした情報源リンク
- Go Issue #3844 (関連する可能性のある情報): https://golang.org/issue/3844 (Web検索結果より、このコミットが修正したIssue #3844は
bytes.Join
のエイリアシング問題に関連していることが示唆されています。)