先日SwiftのArrayがヤバイという投稿を書きましたが(ただし、SwiftのArrayはすばらしく生まれかわりました)、実はGoのSliceでもまったく同じことが起きます。
GoのSliceのヤバイ挙動
次のコードはSwiftのArrayがヤバイで取り上げたコードをGoに書き換えたものです。結果はSwiftのときと同じです。
a := []int{11, 22, 33} // a == [11 22 33]
b := a // b == [11 22 33]
a[0] = 777 // b[0]も777になる
a = append(a, 44)
a[0] = 888 // b[0]は888にならない
appendの結果をaに再代入してるから、そこでSliceの実体が変わってしまってるんだろうと思うかもしれません。しかし、少しだけ変更した次のコードでは、aに再代入しているにも関わらずaとbで実体が共有されています。そして、もう一度appendすると今度は実体が共有されなくなります。
a := make([]int, 3, 4)
a[0], a[1], a[2] = 11, 22, 33 // a == [11 22 33]
b := a // b == [11 22 33]
a[0] = 777 // b[0]も777になる
a = append(a, 44)
a[0] = 888 // b[0]も888になる(←ここが変わった)
a = append(a, 55)
a[0] = 999 // b[0]は999にならない
さらに、GoのSliceでは次のようなおもしろいことも起こります。
a := make([]int, 1, 2)
b := a // a == [0], b == [0]
a = append(a, 1) // a == [0 1], b == [0]
b = append(b, 2) // a == [0 2], b == [0 2](←bにappendするとaの要素が(追加ではなく)変更される)
上記のコードでは、最後の行でbにappendしているのにaの要素が変更されています。このコードも少し変更すると次のように結果が変わります(最後の行のaが変わっています)。
a := make([]int, 1)
b := a // a == [0], b == [0]
a = append(a, 1) // a == [0 1], b == [0]
b = append(b, 2) // a == [0 1], b == [0 2](←ここが変わった)
雑感
このような現象が起こるのは、GoのSliceが値型だけど配列への参照を内部に持っており、append時に必要に応じて参照先を作りなおすからです[*1]。GoのSliceは可変長配列としても使われますが、どちらかと言うと名前のとおり配列をスライスしたものとの意味合いが強いのではないかと思います。配列のスライスが基本で、appendは付加的に用意されていると考えるのであれば、本投稿のタイトルはミスリードかもしれません。
ただ、個人的には、あるものを正しく使うのにその実装まで理解していなければならないというのは好みではありません。GoのSliceを使うには、Sliceや関連する関数の実装を(↑の挙動を当然と思えるくらいに)正しく理解していないと痛い目にあいそうです。Goのような低級言語では、利用者が実装まで知っていなければならないのもある程度仕方ないことかもしれませんが。
おまけ
ちなみに、Sliceの参照(ポインタ)をとれば次のような結果になります。これは、可変長配列が参照型である多くの言語と同様の挙動でわかりやすいかと思います。
a := &[]int{0}
b := a // a == &[0], b == &[0]
*a = append(*a, 1) // a == &[0 1], b == &[0 1]
*b = append(*b, 2) // a == &[0 1 2], b == &[0 1 2]
おまけ2
同じことをSwiftでやると、capacityを2になるように確保しても次のようになります。
a = [0]
a.reserveCapacity(2)
b = a // a == [0], b == [0]
a.append(1) // a == [0, 1], b == [0]
b.append(2) // a == [0, 1], b == [0, 2]
これは、SwiftのArrayが値として振る舞うからです。
[*1] 値型や参照型というのはGoの用語ではないですが、SwiftのArrayがヤバイからの話の流れで、プログラミング言語一般の話としてそのように呼んでいます。