スライスのメモリ共有
スライスからサブスライスを切り出す際はコピーを作成している訳ではなく、スライスとメモリを共有してサブスライスが作成される。
つまり、スライス・サブスライス共に要素を変更するとメモリを共有している箇所が影響を受ける。
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
x[1] = 20
y[0] = 10
z[1] = 30
fmt.Println("x:", x) // x: [10 20 30 4]
fmt.Println("y:", y) // y: [10 20]
fmt.Println("z:", z) // z: [20 30 4]
ここでappendした場合の動きを確認してみる。
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
y = append(y, 10)
z = append(z, 20)
fmt.Println("x:", x) // x: [1 2 10 4]
fmt.Println("y:", y) // y: [1 2 10]
fmt.Println("z:", z) // z: [2 10 4 20]
yにappendした場合、xの3が10に変更されてしまった。これは何が起こっているのだろうか?
それぞれのスライスのキャパシティは以下のようになる。
x := []int{1, 2, 3, 4}
y := x[:2]
z := x[1:]
fmt.Println(cap(x), cap(y), cap(z)) //4 4 3
y = append(y, 10)
z = append(z, 20)
fmt.Println("x:", x) // x: [1 2 10 4]
fmt.Println("y:", y) // y: [1 2 10]
fmt.Println("z:", z) // z: [2 10 4 20]
サブスライスを作成した際のキャパシティは元のスライスのキャパシティからオフセット分引いた値になる。
そのためyは長さは2になるがキャパシティは4になり、yにappendで3つ目の要素を加えるとxの3つ目の要素を上書きしてしまう。
解決策1
この解決策としてサブスライスを作成する際はフルスライス式を使用する。フルスライス式を使用するとサブスライスを作成する際にキャパシティを指定することができる。
サブスライスのキャパシティは元のスライスから利用するキャパシティの最後の位置を指定する。
x := []int{1, 2, 3, 4}
y := x[:2:2]
z := x[1:4:4]
fmt.Println(cap(x), cap(y), cap(z)) //4 2 3
y = append(y, 10)
z = append(z, 20)
fmt.Println("x:", x) // x: [1 2 3 4]
fmt.Println("y:", y) // y: [1 2 10]
fmt.Println("z:", z) // z: [2 3 4 20]
このようにすることで元のスライスに影響を与えることなくappendができるようになる。
解決策2
完全に元のスライスとメモリを共有しないオリジナルのサブスライスを作成する。
copy関数を使うことで元の配列からスライスを必要な分コピーする。
copy関数の戻り値はコピーできた要素数になる。
x := []int{1, 2, 3, 4}
y := make([]int, 2)
z := make([]int, 3)
num_y := copy(y, x[:2]) // xをyにコピーする
// num_y := copy(y, x) // この書き方でもOK キャパシティ分コピーされる
num_z := copy(z, x[1:])
fmt.Println("num:", num_y) // num: 2
fmt.Println("num:", num_z) // num: 3
y = append(y, 10)
z = append(z, 20)
fmt.Println("x:", x) // x: [1 2 3 4]
fmt.Println("y:", y) // y: [1 2 10]
fmt.Println("z:", z) // z: [2 3 4 20]
参考
Goのフルスライス式 a[x:y:z] は何のためにある?
初めてのGo言語 ―他言語プログラマーのためのイディオマティックGo実践ガイド