1つの例で見るスライス
スライスは、配列に対する伸長可能なビューです。リテラルか make で作り、append で伸ばし、len で大きさを読みます。
[]string の角かっこには長さがありません。これが、サイズが固定されている [4]string のような配列型との違いです。
スライスとは何か
スライスの値は、3つのフィールドを持つ小さなヘッダーです。
- 元の配列の要素へのポインタ
- 長さ:スライスから見える要素の数
- 容量:そのポインタから元の配列の終わりまでにある要素の数
スライスをコピーする(代入する、関数に渡す)と、このヘッダーだけがコピーされます。どちらのコピーも同じ要素を指します。スライスで驚かされることのほとんどは、これを忘れることから生まれます。
make:長さと容量
make([]T, len, cap) は元の配列を確保し、それに対するスライスを返します。容量は省略でき、デフォルトは長さと同じです。
よくある混同があります。make([]int, 5) の後に append を5回呼ぶと、要素は10個になり、最初の5つはゼロです。appendするつもりなら make([]int, 0, 5)、インデックスで代入するつもりなら make([]int, 5) を使います。
最終的なサイズがわかっているときに容量を事前に確保すると、何度も拡張するのを避けられます。これは最適化であって、必須ではありません。
appendと拡張
append は現在の長さの後ろに要素を追加します。容量が十分ならその場に書き込みます。足りなければより大きな配列を確保し、古い要素をコピーして、新しい配列を指すスライスを返します。
容量は、スライスが小さいうちはおおよそ2倍になり、256要素を超えるとより小さな倍率(1.25倍に近づいていく)で増えます。正確な数値はランタイムの実装の詳細で、Goのバージョンによって変わってきたので、それに依存するコードは決して書かないでください。
append は別の配列を返すことがあるので、その結果を使う必要があります。
append(s, 4) // compile error: append(s, 4) (value of type []int) is not used
s = append(s, 4) // correct
スライス全体を追加するには展開の構文を使います:s = append(s, other...)。
スライス操作は元の配列を共有する
s[low:high] は同じ配列に対する新しいスライスヘッダーを作り、インデックス low から high の手前までを覆います。どちらの境界も省略できます。要素はコピーされません。
出力:
[10 99 30 40 50]
3 4
この共有こそがスライス操作を安価にしています。そして、最も有名なスライスのバグの原因でもあります。
appendのエイリアシングのバグ
部分スライスに余った容量があると、それに対するappendは親の配列の、親がまだ使っている要素の上に書き込みます。
出力:
[1 2 100 4 5]
[1 2 100]
[cmd test] [cmd test]
a と b は "cmd" の後ろの配列の要素を共有しているので、2回目のappendが1回目を上書きしました。コードは問題なく見え、容量がたまたまいっぱいのときはうまく動くので、このバグはときどきしか現れません。
直し方は2つです。
- 完全スライス式
s[low:high:max]は容量をmax-lowに制限します。余った容量がなければ、次のappendは必ず新しく確保します。first := base[:2:2]とすれば最初の例は安全になります。 - スライスが呼び出しより長く生きたり、独立してappendされたりするときは 明示的にコピーします:
slices.Clone(prefix)やappend([]string(nil), prefix...)。
関連する罠があります。巨大な配列の小さなスライスは、ガベージコレクタに対して配列全体を生かし続けます。100MBのファイルを読んで data[:10] を保持すると、100MBがメモリに残ります。保持する部分はクローンしてください。
copy
組み込みの copy(dst, src) は min(len(dst), len(src)) 個の要素をコピーし、その数を返します。dst を伸ばすことはありません。
copy はコピー元とコピー先が重なっていても正しく処理するので、copy(s[1:], s) は要素を壊さずに右へずらします。
slicesパッケージ
Go 1.21以降、標準の slices パッケージには、以前は手で書いていた操作のためのジェネリックなヘルパーがあります。
| 関数 | すること |
|---|---|
Contains、Index | 値を探す(ContainsFunc、IndexFunc は判定関数を受け取る) |
Sort、SortFunc、SortStableFunc | その場でソートする |
BinarySearch | ソート済みのスライスを検索する |
Insert、Delete、DeleteFunc | 挿入または削除し、新しいスライスを返す |
Compact | 連続する重複を取り除く |
Equal、Compare | 要素ごとに比較する |
Clone、Reverse、Max、Min | 名前のとおり |
Collect、Sorted、Values、All | イテレータを扱う(Go 1.23) |
Delete と Insert は、append と同じく代入が必要なスライスを返します。ソートには専用のソートのページがあります。
インデックスでループしながら要素を削除すると、要素が飛ばされます。代わりに slices.DeleteFunc を使います。
s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens
2次元スライス
スライスのスライスは、行ごとに長さが違ってもよいグリッドになります。各行は個別に確保する必要があります。
内側の make を忘れると各行はnilのままで、grid[1][2] = 7 は index out of range [2] with length 0 でpanicします。
nilスライスと空のスライス
nilスライスは len、cap、range、append に対して空のスライスと同じように振る舞うので、ゼロの状態には var s []T を使いましょう。[]T{} で初期化するのは違いが重要なときだけで、それはほぼJSONの出力(null か [] か)です。空かどうかは s == nil ではなく len(s) == 0 でチェックします。
スライスと関数
スライスを受け取った関数はその要素を変更でき、呼び出し側からもその変更が見えます。しかしヘッダーのコピーを受け取っているので、呼び出し側の長さは変更できません。appendする関数は新しいスライスを返す必要があります。
func addAll(s []int, vals ...int) []int {
return append(s, vals...)
}
append、slices.Delete、slices.Insert がすべてスライスを返すのはこのためです。
よくある間違い
appendの結果を無視する。 まるごと捨てるとコンパイルできませんが、append(s, x)を、使い続ける変数とは別の変数に代入するのはロジックのバグです。- 1つのベースから2回appendする。 記憶領域を共有することがあります。ベースをクローンするか、完全スライス式を使います。
make([]T, n)の後にappendする。 先頭にn個のゼロが残ります。- インデックスの範囲外。
s[len(s)]はpanicします。最後の要素はs[len(s)-1]です。 - スライスを
==で比較する。 コンパイルできるのはs == nilだけです。slices.Equalを使います。
よくある質問
Goのスライスの長さと容量の違いは何ですか?
len(s) はスライスが現在保持している要素の数です。cap(s) は、スライスの最初の要素から数えて元の配列に収まる要素の数です。append は余っている容量があればそこに書き込み、なければより大きな新しい配列を確保します。
Goでスライスに要素を追加するには?
組み込みの append を呼び、結果を代入し直します:s = append(s, x)。複数の値(append(s, 1, 2, 3))や別のスライス(append(s, other...))も追加できます。append は新しい配列を指すスライスを返すことがあるので、必ず戻り値を使ってください。
Goでスライスから要素を削除するには?
slices.Delete(s, i, i+1)(Go 1.21)を使います。後ろの要素を前に詰めて、短くなったスライスを返します。Go 1.22以降は、空いた末尾の要素もゼロにします。順序が重要でなければ、最後の要素と入れ替えて切り詰めます:s[i] = s[len(s)-1]; s = s[:len(s)-1]。
Goでスライスが値を含むか調べるには?
標準の slices パッケージ(Go 1.21)の slices.Contains(s, v) を使います。slices.Index(s, v) は位置か-1を返し、slices.ContainsFunc は判定関数を受け取ります。大きなデータで何度も検索するなら、代わりにマップを作ります。
Goのnilスライスと空のスライスの違いは何ですか?
var s []int はnilで、s := []int{} は空ですがnilではありません。どちらも長さは0で、len、range、append で使えます。違いが出るのは s == nil のチェックと encoding/json で、nilスライスは null、空のスライスは [] にエンコードされます。