Goのスライスから値を指定して要素を削除する方法——slices.DeleteFuncも解説

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

Go言語でスライスから特定の値を持つ要素を削除したいとき、Rubyの delete メソッドのような便利な組み込み関数がないため、どう書けばよいか迷いました。Go 1.21以降なら slices.DeleteFunc が使え、従来の append を使った手法と合わせて押さえておくと迷わなくなります。

なお、「インデックスを指定して削除する」方法については別記事で解説しています。

どちらの方法を使うか

削除の基準推奨方法
特定の値・条件に一致する要素slices.DeleteFunc(この記事)
n番目の要素を位置で指定append + インデックス操作(別記事)

Go 1.21以降:slices.DeleteFuncを使う(推奨)

Go 1.21で追加された slices.DeleteFunc を使うと、条件に一致する要素をループなしで削除できます。自分はJANコード(商品識別子)で特定の商品をスライスから取り除く処理で初めてこれを使いました。

import "slices"

type Product struct {
    JAN string
}

products := []Product{
    {JAN: "4901234567890"},
    {JAN: "hogeCode"},
    {JAN: "4901234567891"},
}

deleteTargetJan := "hogeCode"

products = slices.DeleteFunc(products, func(p Product) bool {
    return p.JAN == deleteTargetJan
})
// products => [{4901234567890} {4901234567891}]

コールバック関数が true を返した要素をすべて取り除きます。インデックスのズレを気にする必要がなく、複数の一致があってもまとめて削除できます。Go 1.21以降ではこの方法を最初に検討するのがよいと思います。

Go 1.21未満:appendを使う

Go 1.21未満の環境では、appendcopy を使ってスライスを操作します。

順序を保ったまま削除する

// i番目の要素を削除(順序を維持)
a = append(a[:i], a[i+1:]...)

a[:i] で先頭から i-1 番目までを取り出し、a[i+1:]... で i+1 番目以降を展開して追記します。結果として i 番目が抜けたスライスになります。

もう一つの書き方として copy を使う方法もあります。

a = a[:i+copy(a[i:], a[i+1:])]

copy(a[i:], a[i+1:]) は i 番目以降を i+1 番目以降で上書きし、コピーした要素数を返します。a[:i+コピー数] で末尾の重複要素を切り落とします。

順序が変わってよい場合

a[i] = a[len(a)-1]
a = a[:len(a)-1]

末尾の要素を削除したい位置に上書きし、スライスの長さを1減らします。コピーが発生しないため、順序を問わない場合は最も軽い方法です。

参考:

値の一致で削除するときのループ

slices.DeleteFunc が使えない環境で値の一致による削除を行うには、後ろから前にループするか、別スライスにフィルタリングする書き方が安全です。

deleteTargetJan := "hogeCode"

// 後ろから前に走査してインデックスずれを回避する
for i := len(products) - 1; i >= 0; i-- {
    if products[i].JAN == deleteTargetJan {
        products = append(products[:i], products[i+1:]...)
    }
}

前から走査しながら append でスライスを縮めるとインデックスがずれるため、後ろから走査するのが確実です。

slices.DeleteFuncの内部動作

slices.DeleteFunc は内部的に「残す要素だけを前に詰めていく」アルゴリズム(いわゆる stable partition)で動作します。このため元のスライスがインプレースで書き換えられます。

products := []Product{
    {JAN: "A"},
    {JAN: "B"},
    {JAN: "A"},
}
// BとAが混在。A を削除すると B だけが残る
products = slices.DeleteFunc(products, func(p Product) bool {
    return p.JAN == "A"
})
// products => [{B}]

戻り値を再代入しないと元の変数は長さが変わらないまま「ゴミ要素」が末尾に残るため、必ず products = slices.DeleteFunc(...) として戻り値を受け取る必要があります。

また、slices.DeleteFunc の後に元スライスの末尾要素を参照すると削除済みデータにアクセスしてしまうことがあります。Go 1.22 ではゼロ値クリアが入りましたが、それ以前のバージョンでは末尾に不要なデータが残る点を把握しておくと安全です。

filter パターン:別スライスに残す

元スライスを書き換えたくない場合や、slices.DeleteFunc が使えない環境では、残したい要素を別スライスに収集するパターンが明快です。

filtered := products[:0]  // 元のメモリを再利用しながら長さをゼロにする
for _, p := range products {
    if p.JAN != deleteTargetJan {
        filtered = append(filtered, p)
    }
}

products[:0] は同じバッキング配列を使いながら長さゼロのスライスを作るため、追加時の再アロケーションを減らせます。ただし元スライスと同じ配列を参照しているため、元スライスの値も上書きされます。完全に分離したいときは make([]Product, 0, len(products)) で新しいスライスを作ります。

質問・リクエストを送る

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

このサイトについて

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