はじめに
- Goのスライスは動的な配列として利用でき、要素数が固定されていないため、柔軟に扱えます
- ただし、
append
を使用して要素を追加する際には、スライスのキャパシティがどのように影響するかを理解しておく必要があります - 本記事では、
append
の動作がスライスのキャパシティによってどのように変わるかを解説し、そのキャパシティ依存の問題を回避する方法の一つとしてフルスライス式を紹介します
本記事で話さないこと
本記事では挙動の違いについて理解することを重視するため、スライスの内部構造や、ポインタ、Goランタイムの正確な挙動については説明を省略します。
より詳細に知りたい方は、3. 関数と型 - 3.1. 型 - Gopher道場 -をご覧ください。スライスについては29:47〜、append
については44:45〜から解説されています。
appendの基本的な動作
まず、append
の基本的な使い方を見てみましょう。
package main
import "fmt"
func main() {
x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
x = append(x, 1, 2, 3, 4)
y := x[:2]
z := x[2:]
fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 5
fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3
}
このソースコードでは、長さ0、キャパシティ5のスライスx
を作成し、append
で1から4までの値を追加します。その結果、x
の内容は[1 2 3 4]
となり、長さは4、キャパシティは5となります。
y
とz
は、x
のサブスライス(スライスのスライス)で、それぞれ[1 2]
と[3 4]
を指しています。
余談
y
とz
のキャパシティの値が異なる点について簡単に補足します。スライス後のキャパシティはスライス元のキャパシティ - スライスの開始位置
となります。そのため、y
のキャパシティは5 - 0 = 5
、z
のキャパシティは5 - 2 = 3
となります。
キャパシティが足りない場合の挙動
では、キャパシティが足りない場合の挙動から見てみましょう。
package main
import "fmt"
func main() {
x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
x = append(x, 1, 2, 3, 4)
y := x[:2]
z := x[2:]
y = append(y, 30, 40, 50, 60)
fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50 60] 6 10
fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3
}
このコードでは、y
に30、40、50、60を追加しますが、60を追加する際にキャパシティが不足します。そのため、append
は新しいスライスを生成し、その際にキャパシティが倍増して10になります(必ず倍増するわけではありませんが)。結果として、y
の内容は[1 2 30 40 50 60]
となります。
注目したいのは、この場合、y
の要素の変更がx
やz
に影響しないということです。
キャパシティが足りている場合の挙動
次に、キャパシティが足りている場合のappend
の挙動を見てみましょう。
package main
import "fmt"
func main() {
x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
x = append(x, 1, 2, 3, 4)
y := x[:2]
z := x[2:]
// fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
// fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 5
// fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 3
y = append(y, 30, 40, 50)
fmt.Println(x, len(x), cap(x)) // 出力: [1 2 30 40] 4 5
fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50] 5 5
fmt.Println(z, len(z), cap(z)) // 出力: [30 40] 2 3
}
このコードでは、y
に30、40、50を追加しますが、y
の元のキャパシティは5のままで、append
した結果も問題なく保存されています。一方で、x
のスライスの内容は[1 2 30 40]
、z
のスライスの内容は[30 40]
と、影響を受けていることが分かるかと思います。
ここでの注目点は2点あります。
-
y
での要素の変更が、x
やz
にも影響を与える -
y
に要素を追加しても、x
やz
に影響はない-
x
とz
について、30 40と要素が変更されている一方で、60の追加はされていません
-
解説:なぜ挙動に差が出るのか?
どうしてキャパシティ依存でappend
の挙動に差が出るのか、改めて説明しようと思います。
キャパシティが足りない場合のappendの動作
キャパシティが足りない場合、Goランタイムは追加のメモリを確保して新しい配列を作成し、元のデータをそこにコピーしてから新しい要素を追加します。
そのため、append
の結果として生成される新しいスライスは、元のスライス(x
)とは異なる配列を参照するようになり、他のスライスには影響を与えません。
キャパシティが足りている場合のappendの動作
スライスのキャパシティが足りている場合、appendを使っても新たにメモリは確保されません(これはメモリ管理を効率的に行うためです)。
そのため、新しい配列が作成されることなく、既存の配列に要素が追加されます。これにより、元のスライス(x
)とそのサブスライス(y
やz
)は同じ配列を参照し続けるため、y
やz
の要素を変更すると、x
にもその変影響を与えることになります。
このように、キャパシティが足りているかどうかでappend
の動作が異なるため、挙動に差が生まれます。
append時のキャパシティ依存問題の解決策:フルスライス式
append
のキャパシティ依存問題を回避する方法として、フルスライス式を紹介します。次のソースコードを見てください。
package main
import "fmt"
func main() {
x := make([]int, 0, 5) // 長さ0、キャパシティ5のスライス
x = append(x, 1, 2, 3, 4)
y := x[:2:2] // フルスライス式を使用
z := x[2:4:4] // フルスライス式を使用
// fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
// fmt.Println(y, len(y), cap(y)) // 出力: [1 2] 2 2
// fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 2
y = append(y, 30, 40, 50)
fmt.Println(x, len(x), cap(x)) // 出力: [1 2 3 4] 4 5
fmt.Println(y, len(y), cap(y)) // 出力: [1 2 30 40 50] 5 6
fmt.Println(z, len(z), cap(z)) // 出力: [3 4] 2 2
}
このソースコードでは、y
とz
をフルスライス式で作成しました。
フルスライス式は次のように記述します。
// low = スライスの開始位置
// high = スライスの終了位置(このインデックスの手前までが含まれる)
// max = スライス元の参照上限位置(キャパシティは「max- low」で決まる)
slice[low : high : max]
- y := x[:2:2]
-
x[:2]
で、x
の先頭から2番目までの要素を参照 - 最後の
:2
は、スライス元(x
)の参照上限位置を2に制限 -
y
のキャパシティは2 - 0 = 2
-
- z := x[2:4:4]
-
x[2:4]
で、x
の3番目と4番目の要素を参照 - 最後の
:4
は、スライス元(x
)の参照上限位置を4に制限 -
z
のキャバシティは4 - 2 = 2
-
フルスライス式ではキャパシティを明示的に制限することで、y
への要素追加がx
やz
には影響を与えません。(※要素の変更による影響は依然と残ります)
まとめ
- Goのスライスにおける
append
の基本的な動作や、キャパシティによる挙動の違いについて説明しました - キャパシティ不足時は新しいスライスが作成され、キャパシティが十分な場合は同じ配列が参照され続けます
- フルスライス式を使用することで、キャパシティ依存による影響を最小限に抑え、
append
時の上書きを避けることが可能です
参考
-
The Go Programming Language Specification
- Slice expressions
- Full slice expressions
-
初めてのGo言語 —— 他言語プログラマーのためのイディオマティックGo実践ガイド
- 3.2.6 スライスのスライス
- 6.7 マップとスライスの違い
-
- スライス(29:47〜)
- appendの挙動(44:45〜)