Menu

Go言語のスライス:append、make、lenとcap、共有によるバグ

スライスはGoで日常的に使うリスト型です。make、appendと容量の拡張、lenとcapの違い、スライス操作が元の配列を共有する仕組み(とそれが生むバグ)、copy、slicesパッケージ、2次元スライス、nilと空の違いを解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

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]

ab"cmd" の後ろの配列の要素を共有しているので、2回目のappendが1回目を上書きしました。コードは問題なく見え、容量がたまたまいっぱいのときはうまく動くので、このバグはときどきしか現れません。

直し方は2つです。

  1. 完全スライス式 s[low:high:max] は容量を max-low に制限します。余った容量がなければ、次の append は必ず新しく確保します。first := base[:2: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 パッケージには、以前は手で書いていた操作のためのジェネリックなヘルパーがあります。

関数すること
ContainsIndex値を探す(ContainsFuncIndexFunc は判定関数を受け取る)
SortSortFuncSortStableFuncその場でソートする
BinarySearchソート済みのスライスを検索する
InsertDeleteDeleteFunc挿入または削除し、新しいスライスを返す
Compact連続する重複を取り除く
EqualCompare要素ごとに比較する
CloneReverseMaxMin名前のとおり
CollectSortedValuesAllイテレータを扱う(Go 1.23)

DeleteInsert は、append と同じく代入が必要なスライスを返します。ソートには専用のソートのページがあります。

インデックスでループしながら要素を削除すると、要素が飛ばされます。代わりに slices.DeleteFunc を使います。

s = slices.DeleteFunc(s, func(n int) bool { return n%2 == 0 }) // drop evens

2次元スライス

スライスのスライスは、行ごとに長さが違ってもよいグリッドになります。各行は個別に確保する必要があります。

内側の make を忘れると各行はnilのままで、grid[1][2] = 7index out of range [2] with length 0 でpanicします。

nilスライスと空のスライス

nilスライスは lencaprangeappend に対して空のスライスと同じように振る舞うので、ゼロの状態には var s []T を使いましょう。[]T{} で初期化するのは違いが重要なときだけで、それはほぼJSONの出力(null[] か)です。空かどうかは s == nil ではなく len(s) == 0 でチェックします。

スライスと関数

スライスを受け取った関数はその要素を変更でき、呼び出し側からもその変更が見えます。しかしヘッダーのコピーを受け取っているので、呼び出し側の長さは変更できません。appendする関数は新しいスライスを返す必要があります。

func addAll(s []int, vals ...int) []int {
	return append(s, vals...)
}

appendslices.Deleteslices.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で、lenrangeappend で使えます。違いが出るのは s == nil のチェックと encoding/json で、nilスライスは null、空のスライスは [] にエンコードされます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める