Goのスライスから要素を削除する方法——Go 1.21以降はslices.Deleteが使える

Goのスライスから要素を削除する方法——Go 1.21以降はslices.Deleteが使える

6分で読めるテック
最終更新:

Go でスライスの要素をインデックス指定で削除するには、Go のバージョンによって推奨の書き方が変わります。「golang スライス 要素 削除」で調べてさまざまな方法が出てきて混乱した経験があったので、バージョン別に整理しておきます。

Go バージョン推奨方法
1.21 以降slices.Delete(s, i, i+1)
1.20 以前append(a[:i], a[i+1:]...)

Go 1.21 以降:slices.Delete(推奨)

Go 1.21 で追加された標準ライブラリ slices パッケージの Delete 関数を使うのが現在の推奨です。

import "slices"

a := []int{1, 2, 3, 4, 5}

// インデックス 2 の要素(3)を削除
a = slices.Delete(a, 2, 3)
// => [1 2 4 5]

slices.Delete(s, i, j)s[i:j] の範囲を削除します。1 要素だけ削除するときは j = i + 1 にします。第 2 引数が「含む」、第 3 引数が「含まない」という半開区間になっているので、slices.Delete(a, 2, 3) はインデックス 2 の要素 1 つだけを取り除きます。

公式パッケージリファレンスはこちらです。

slices.Delete の内部動作と注意点

slices.Deleteappend(s[:i], s[j:]...) と等価な処理をしており、インプレースで元のスライスのメモリを書き換えます。これは元のスライスを参照している別の変数に影響する可能性があるため注意が必要です。

a := []int{1, 2, 3, 4, 5}
b := a  // 同じ配列を参照

a = slices.Delete(a, 2, 3)
// a => [1 2 4 5]
// b => [1 2 4 5 5](末尾が「汚染」される)

削除後の末尾要素は未定義状態になるため、slices.Delete を呼んだ後の元スライスの末尾要素には依存しないようにします。Go 1.22 以降では slices.Delete がゼロ値でクリアするよう修正されましたが、環境に依存するため戻り値のスライスだけを使うのが安全です。

Go 1.20 以前:append トリック

Go 1.20 以前や slices パッケージを使わない環境では、append を組み合わせて削除します。

順序を保持する

a := []int{1, 2, 3, 4, 5}
i := 2  // 削除したいインデックス

a = append(a[:i], a[i+1:]...)
// => [1 2 4 5]

a[:i] で削除位置より前の部分を取り、そこに a[i+1:]... (削除位置より後ろの要素)を追加しています。結果として i 番目の要素が飛んだスライスになります。

このパターンは append の第 1 引数 a[:i]a[i+1:] が同じ元配列を指しているため、copy が内部で最適化されます。ただし a[:i] のキャパシティは元の a と同じなので、len(a[:i]) < cap(a[:i]) が成立し、新しいメモリを確保せずに済みます。

Go wiki の SliceTricks に詳しく載っています。

順序を保持しない(高速)

順序が変わってもよい場合は、末尾要素と入れ替える方法が高速です。

a[i] = a[len(a)-1]
a = a[:len(a)-1]
// => [1 2 5 4](順序が変わる)

末尾の要素を削除位置に上書きしてからスライスを 1 つ短くするだけなので、要素のコピーが 1 回で済みます。要素数が多く順序に意味がないケースでは、こちらのほうがパフォーマンス上有利です。

ポインタや参照型を要素に持つスライスの場合、末尾に残った要素がGCで回収されないメモリリークの原因になることがあります。そのような場合は削除後に末尾をゼロ値にしておくと安全です。

a[i] = a[len(a)-1]
var zero int
a[len(a)-1] = zero  // 末尾をゼロ値でクリア
a = a[:len(a)-1]

複数インデックスをまとめて削除したい場合

複数の要素を一度に削除したいときは、slices.Delete(s, i, j) の範囲削除を使うか、削除対象が連続していない場合は slices.DeleteFunc で条件フィルタリングします。

// インデックス 1〜3 の範囲を一括削除
a = slices.Delete(a, 1, 4)

// 複数の特定インデックスを削除(後ろから順に削除するとインデックスずれを防げる)
indices := []int{4, 2, 0}  // 必ず降順にする
for _, idx := range indices {
    a = slices.Delete(a, idx, idx+1)
}

複数インデックスを削除する場合、前から削除するとインデックスがずれるため、後ろ(大きいインデックス)から順に処理するのが基本です。

値を指定して削除したい場合

「インデックスではなく値を指定して削除したい」という場合は、slices.Indexslices.Delete を組み合わせるか、slices.DeleteFunc を使います。こちらの詳しい解説は別記事にまとめています。

参考

質問・リクエストを送る

記事についての質問や、取り上げてほしいテーマがあればお気軽にどうぞ。いただいた質問はブログ記事として回答し、Q&Aページで公開することがあります。

このサイトについて

井上 周(Amane Inoue)の個人ブログです。技術・読書・ドラマ・旅・大学生活のことを書いています。