Go のスライスの内部構造(長さ・容量・ポインタ)

スライスを正しく使いこなすには、その内部構造を理解することが重要です。スライスは単なる可変長配列ではなく、背後にある配列を参照する「ビュー」として動作します。

スライスの3つの要素

スライスの内部は、以下の3つの情報で構成されています。

ポインタ(ptr)

背後にある配列の、スライスが参照する最初の要素へのポインタ。

長さ(len)

スライスに含まれる要素の数。len() 関数で取得できる。

容量(cap)

スライスの先頭から、背後の配列の末尾までの要素数。cap() 関数で取得できる。

arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4] // [20, 30, 40]

fmt.Println(len(slice)) // 3
fmt.Println(cap(slice)) // 4

この例では、スライスはインデックス 1 から始まり、背後の配列の末尾(インデックス 4)まで 4 要素分の容量を持っています。

図で理解する内部構造

配列から作ったスライスの関係を図示すると、以下のようになります。

arr := [5]int{10, 20, 30, 40, 50}
slice := arr[1:4]

背後の配列 arr に対して、slice は途中の部分を「窓」として覗いている状態です。ポインタは arr[1] を指し、長さは 3、容量は 4 となります。

背後の配列を共有する

同じ配列から複数のスライスを作ると、それらは背後の配列を共有します。どれかのスライスで値を変更すると、他のスライスにも影響します。

arr := [5]int{10, 20, 30, 40, 50}
slice1 := arr[1:4]
slice2 := arr[2:5]

slice1[1] = 999 // arr[2] を変更

fmt.Println(arr)    // [10 20 999 40 50]
fmt.Println(slice1) // [20 999 40]
fmt.Println(slice2) // [999 40 50]

この挙動は便利な場面もありますが、意図しない変更が発生する原因にもなります。

make で作成した場合

make でスライスを作成すると、Go が自動的に背後の配列を確保します。この配列は外部から直接参照できません。

slice := make([]int, 3, 5)
fmt.Println(len(slice)) // 3
fmt.Println(cap(slice)) // 5

長さ 3、容量 5 のスライスが作られ、背後には 5 要素分の配列が確保されています。長さを超えた部分(インデックス 3、4)はまだアクセスできませんが、append で追加する際の予備領域として機能します。

容量が重要な理由

容量を意識することで、パフォーマンスを最適化できます。append で要素を追加する際、容量が足りなくなると新しい配列が確保され、既存の要素がコピーされます。あらかじめ十分な容量を確保しておけば、このコピー処理を減らせます。