Goにおけるスライスを理解する:動的配列の実践
James Reed
Infrastructure Engineer · Leapcell

Goのスライス型は、多くのGoプログラムの中心にある強力で柔軟な構造です。しばしば概念的に動的配列と比較されますが、スライス自体は配列ではなく、基盤となる配列へのビューあるいは参照であるということを理解することが重要です。この区別は、その動作、パフォーマンス特性、そしてそれらを効果的に使用する方法を把握する鍵となります。
スライスとは何か?配列へのビュー
その核となるGoのスライスは、3つのコンポーネントで構成されるデータ構造です。
- ポインタ(Pointer): スライスが参照する基盤となる配列の最初の要素を指し示します。これは必ずしも基盤となる配列自体の始まりではありませんが、スライスのビューの開始点です。
- 長さ(len): これは現在スライスを通してアクセス可能な要素の数です。これはビューの長さを表します。
- 容量(cap): これは、スライスのポインタから始まる基盤となる配列内の要素数で、再割り当てなしでスライスが使用できるものです。これは潜在的なビューの最大範囲を表します。
視覚的に、メモリ内の基盤となる配列を想像してみてください。スライスは、その配列の連続した部分に対するウィンドウを定義するだけです。
// 基盤となる配列 var underlyingArray [10]int // underlyingArrayのセグメントをビューするスライス's' // sはunderlyingArrayのインデックス2を指します // sは長さ3(インデックス2、3、4の要素)を持ちます // sは容量8(インデックス2から9までの要素)を持ちます s := underlyingArray[2:5]
この設計は計り知れない柔軟性を提供します。複数のスライスが同じ基盤となる配列を参照でき、潜在的に重複したり、異なるセグメントをビューしたりすることができます。この動作は、copy
操作や、一方のスライスへの変更が他方のスライスを通して見える可能性があることを理解する上で重要です。
スライスの作成
Goでスライスを作成するにはいくつかの方法があります。
1. 既存の配列またはスライスのスライス
これは、既存の配列またはスライスを活用してスライスを作成する最も一般的な方法です。a[low:high]
構文は、low
からhigh
(high
は含まない)までの要素を含むスライスを作成します。
arr := [5]int{10, 20, 30, 40, 50} // インデックス1(含む)からインデックス4(含まない)までのスライス s1 := arr[1:4] // s1 == {20, 30, 40} fmt.Printf("s1: %v, len: %d, cap: %d\n", s1, len(s1), cap(s1)) // s1: [20 30 40], len: 3, cap: 4 (20~50の要素が利用可能) // スライスはlowとhighの境界を省略できます: s2 := arr[2:] // s2 == {30, 40, 50}, len: 3, cap: 3 s3 := arr[:3] // s3 == {10, 20, 30}, len: 3, cap: 5 s4 := arr[:] // s4 == {10, 20, 30, 40, 50}, len: 5, cap: 5 // 他のスライスのスライス: s5 := s1[1:] // s5 == {30, 40}, len: 2, cap: 3 (s1の基盤となる要素30~50が利用可能)
スライスによって作成された新しいスライスの容量は、元のスライス/配列の容量からlow
インデックスを引いたものによって決定されます。これにより、新しいスライスが元のスライスまたは配列の境界外の要素にアクセスしないことが保証されます。
2. make()
の使用
make()
関数は、指定された長さとオプションの容量でスライスを作成するために使用されます。make
がスライスを作成するとき、メモリに新しい基盤となる配列を割り当てます。
// 長さ5、容量5の整数スライスを作成 // すべての要素はゼロ値(intの場合は0)に初期化されます s := make([]int, 5) // s == {0, 0, 0, 0, 0}, len: 5, cap: 5 fmt.Printf("s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // 長さ3、容量10の文字列スライスを作成 // 追加の容量は、再割り当てなしでappend操作に使用できます s2 := make([]string, 3, 10) // s2 == {"", "", ""}, len: 3, cap: 10 fmt.Printf("s2: %v, len: %d, cap: %d\n", s2, len(s2), cap(s2))
3. コンポジットリテラルの使用
配列と同様に、コンポジットリテラルを使用してスライスを直接初期化できますが、サイズを指定する必要はありません。Goは提供された要素に基づいて長さと容量を推論します。新しい基盤となる配列が割り当てられます。
scores := []int{100, 95, 80, 75} // scores == {100, 95, 80, 75}, len: 4, cap: 4 fmt.Printf("scores: %v, len: %d, cap: %d\n", scores, len(scores), cap(scores)) names := []string{"Alice", "Bob", "Charlie"} // names == {"Alice", "Bob", "Charlie"}
必須のスライス操作
1. len(s)
:現在の長さ
len()
組み込み関数は、スライスに现在存在する要素の数を返します。これはスライスの「可視」サイズです。
mySlice := []int{1, 2, 3, 4, 5} fmt.Println(len(mySlice)) // 出力:5 subSlice := mySlice[1:3] // {2, 3} fmt.Println(len(subSlice)) // 出力:2
2. cap(s)
:基盤となる容量
cap()
組み込み関数は、スライスの容量を返します。これは、スライスのポインタから始まる基盤となる配列内の要素数で、スライスが利用できるものです。これは、基盤となる配列がいつ再割り当てされるかを理解する上で重要です。
mySlice := []int{1, 2, 3, 4, 5} fmt.Println(cap(mySlice)) // 出力:5 (初期状態では、リテラルのlen == cap) subSlice := mySlice[1:3] // {2, 3} fmt.Println(cap(subSlice)) // 出力:4 (mySliceの配列のインデックス1から4までの要素) anotherSlice := make([]int, 2, 10) // len:2, cap:10 fmt.Println(len(anotherSlice), cap(anotherSlice)) // 出力:2 10
3. append(s, elems...)
:要素の追加
append()
組み込み関数は、スライスに新しい要素を追加する主な方法です。概念的には、元の要素と新しい要素を含む新しいスライスを返します。2つのシナリオがあります。
- 十分な容量: スライスの容量が新しい要素を収容するのに十分な場合、
append
は単にスライスの長さを拡張し、既存の基盤となる配列を変更します。返されるスライスは、更新された長さを持つ同じ基盤となる配列を指す可能性が高いです。 - 不十分な容量: 容量が不十分な場合、
append
は新しい、より大きな基盤となる配列を割り当てます。既存のすべての要素を古い配列から新しい配列にコピーし、新しい要素を追加してから、この新しい配列を指すスライスを返します。古い基盤となる配列(およびそれを指すスライス)は、他に参照が存在しない場合、ガベージコレクションの対象となります。
基盤となる配列のGoの成長戦略は、通常、再割り当てが必要な場合に容量を2倍にすることです。ただし、これはあるしきい値までであり、非常に大きなスライスではより小さい係数(例:1.25倍)になります。これにより、再割り当てのコストが償却されます。
var numbers []int // nilスライス、len:0, cap:0 numbers = append(numbers, 10) // numbers: [10], len:1, cap:1 (新しい基盤となる配列) fmt.Printf("10後: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) numbers = append(numbers, 20) // numbers: [10 20], len:2, cap:2 (新しい基盤となる配列、通常は2倍) fmt.Printf("20後: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) numbers = append(numbers, 30, 40) // numbers: [10 20 30 40], len:4, cap:4 (再び新しい基盤となる配列) fmt.Printf("30, 40後: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers)) // スライスを別のスライスに追加するには '...' が必要です moreNumbers := []int{50, 60} numbers = append(numbers, moreNumbers...) // numbers: [10 20 30 40 50 60] fmt.Printf("スライス追加後: %v, len: %d, cap: %d\n", numbers, len(numbers), cap(numbers))
append
に関する重要事項: append
は新しいスライスを返す可能性がある(異なる基盤となる配列を指す)ため、その結果を元のスライス変数に再割り当てすることが非常に重要です。これを怠ると、元のスライスは変更されないまま(または古い、潜在的に満杯の基盤となる配列を参照したまま)になります。
s := []int{0, 1, 2} fmt.Printf("append前 s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // append前 s: [0 1 2], len: 3, cap: 3 // このappendは再割り当てを行いNEWスライスを返しますが、`s`は古いものを指したままです。 append(s, 3, 4) fmt.Printf("未再割り当てappend後 s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // 未再割り当てappend後 s: [0 1 2], len: 3, cap: 3 // 正しい方法:結果を元に再割り当てします s = append(s, 3, 4) fmt.Printf("再割り当てappend後 s: %v, len: %d, cap: %d\n", s, len(s), cap(s)) // 再割り当てappend後 s: [0 1 2 3 4], len: 5, cap: 6 (またはGoのバージョン/アーキテクチャによって8)
4. copy(dst, src)
:要素のコピー
copy()
組み込み関数は、ソーススライス(src
)からデスティネーションスライス(dst
)に要素をコピーします。min(len(src), len(dst))
個の要素をコピーします。copy
は新しい基盤となる配列を割り当てません。既存の配列で動作します。
source := []int{10, 20, 30, 40, 50} destination := make([]int, 3) // destination: {0, 0, 0} n := copy(destination, source) // sourceからdestinationに3つの要素(10、20、30)をコピーします fmt.Printf("コピーされた %d 個の要素\n", n) // 出力:コピーされた 3 個の要素 fmt.Printf("source: %v\n", source) // source: [10 20 30 40 50] fmt.Printf("destination: %v\n", destination) // destination: [10 20 30] // sourceで利用可能な数よりも多い、またはdestinationが保持できる数よりも少ないコピー: destination2 := make([]int, 10) copy(destination2, source) // sourceからdestination2にすべての5つの要素をコピーします fmt.Printf("destination2: %v\n", destination2) // destination2: [10 20 30 40 50 0 0 0 0 0] // 自己コピー(シフトのようなインプレース変更に使用可能) s := []int{1, 2, 3, 4, 5} copy(s[1:], s[0:]) // 要素をシフトします:s[1]はs[0]を取得し、s[2]はs[1]を取得するなど。 fmt.Printf("シフトされたスライス: %v\n", s) // シフトされたスライス: [1 1 2 3 4]
copy
は次によく使用されます。
- スライスデータの真に独立したコピーを作成する。
- カスタムスライス操作(挿入、削除、フィルタリングなど)を実装する。
スライスの落とし穴とベストプラクティス
1. 基盤となる配列の変更
スライスはビューであるため、一方のスライスを通して要素を変更すると、変更がそれぞれのビューの範囲内にある限り、同じ基盤となる配列を共有する他のスライスにも影響します。
arr := [5]int{1, 2, 3, 4, 5} s1 := arr[1:4] // s1 == {2, 3, 4} s2 := arr[2:5] // s2 == {3, 4, 5} fmt.Printf("初期状態: s1=%v, s2=%v, arr=%v\n", s1, s2, arr) s1[1] = 99 // これはarr[2]を変更します fmt.Printf("s1[1]=99後: s1=%v, s2=%v, arr=%v\n", s1, s2, arr) // 出力: // 初期状態: s1=[2 3 4], s2=[3 4 5], arr=[1 2 3 4 5] // s1[1]=99後: s1=[2 99 4], s2=[99 4 5], arr=[1 2 99 4 5]
この動作は通常効率のために望ましいですが、意図しない副作用を避けるためには慎重な検討が必要です。完全に独立したコピーが必要な場合は、append
またはcopy
を使用してください。
original := []int{1, 2, 3} // 真に独立したコピーを作成 independentCopy := make([]int, len(original), cap(original)) copy(independentCopy, original) independentCopy[0] = 99 fmt.Printf("Original: %v, Independent Copy: %v\n", original, independentCopy) // 出力:Original: [1 2 3], Independent Copy: [99 2 3]
2. サブスライスによる「メモリリーク」
大きな基盤となる配列から小さなサブスライスを取り出して、そのサブスライスだけを保持する場合、サブスライスがまだ参照を保持しているため、元の大きな配列がガベージコレクションされない可能性があるという一般的な懸念があります。これにより、必要以上に多くのメモリを保持することになる可能性があります。
func createBigSlice() []byte { bigData := make([]byte, 1<<20) // 1MBスライス // ... bigDataをポピュレート ... return bigData[500:510] // 中央からの小さなスライスを返す } // createBigSlice()から返される10バイトのスライスが到達可能な限り、基盤となる1MBの配列はメモリに残ります。
これを防ぐために、大きなスライスの小さな部分のみが必要で、残りをガベージコレクションしたい場合は、copy
を使用して小さなスライスに対して新しい基盤となる配列を作成します。
func createSmallIndependentSlice(bigData []byte) []byte { smallSlice := bigData[500:510] // 独自の基盤となる配列を持つ新しいスライスを作成します independentSmallSlice := make([]byte, len(smallSlice)) copy(independentSmallSlice, smallSlice) return independentSmallSlice } // これで、他に参照が存在しない場合、'bigData'スライスはガベージコレクションできるようになります。
3. Nilスライスと空スライスの比較
- Nilスライス:
var s []int
またはs := []int(nil)
。len == 0
およびcap == 0
を持ちます。nilスライスに対してlen
、cap
、append
、range
を呼び出すことは安全です。 - 空スライス:
s := []int{}
またはs := make([]int, 0)
。len == 0
およびcap == 0
([]int{}
の場合)または指定された容量(make
の場合)を持ちます。
多くのコンテキストで機能的には似ていますが、nilスライスは真に「基盤となる配列がない」ことを表しますが、空スライスはゼロ長の配列を指す可能性があります。nilスライスをスライスの「ゼロ値」として使用することは、append
がそれらを正しく処理するため、一般的に良い習慣です。
var nilSlice []int emptySlice := []int{} madeEmptySlice := make([]int, 0) fmt.Printf("nilSlice: %v, len: %d, cap: %d, is nil: %t\n", nilSlice, len(nilSlice), cap(nilSlice), nilSlice == nil) fmt.Printf("emptySlice: %v, len: %d, cap: %d, is nil: %t\n", emptySlice, len(emptySlice), cap(emptySlice), emptySlice == nil) fmt.Printf("madeEmptySlice: %v, len: %d, cap: %d, is nil: %t\n", madeEmptySlice, len(madeEmptySlice), cap(madeEmptySlice), madeEmptySlice == nil) // すべてappendに安全です nilSlice = append(nilSlice, 1) fmt.Printf("append後 nilSlice: %v\n", nilSlice) // 出力:[1]
結論
Goのスライスは、要素のシーケンスを扱うための基本的で高度に最適化されたデータ型です。その基盤となる配列モデルとlen
、cap
、append
、copy
のセマンティクスを理解することで、効率的で簡潔なGoプログラムを構築するための強力なツールを得られます。スライスはビューであることを常に覚えておいてください。この中心的な原則は、その動作を予測し、一般的な落とし穴を回避するのに役立ちます。スライスをマスターすることは、熟練したGo開発者になるための重要なステップです。