Go言語はシンプルで効率的な言語設計を特徴としていますが、他の多くの言語にあるようなスライスの要素を削除する専用のメソッドは標準で提供されていません。

そのため、開発者はスライスの内部構造を理解し、適切なロジックを用いて要素の削除を行う必要があります。

この記事では、Go言語でスライスから要素を削除するための多様なテクニックを、パフォーマンスやメモリ効率の観点から詳しく紹介します。

2026年現在の標準的な手法から、特定条件下で役立つ高度な最適化まで、実戦で役立つ知識を身に付けましょう。

Go言語におけるスライスの基本構造と削除の仕組み

スライスから要素を削除する方法を学ぶ前に、まずはGo言語のスライスが内部的にどのように動作しているかを理解することが重要です。

スライスは、「配列へのポインタ」「長さ(len)」「容量(cap)」の3つの要素で構成されるデータ構造です。

スライスから要素を削除するという操作は、実際には元の配列の要素を詰め替え、スライスの長さを再定義するというプロセスを指します。

Go言語には組み込みの delete 関数がありますが、これはマップ(map)専用であり、スライスには使用できない点に注意してください。

スライスの要素を削除する際は、主に append 関数や copy 関数、あるいは標準ライブラリの slices パッケージを使用します。

インデックスを指定して1つの要素を削除する標準的な方法

特定のインデックスにある要素を削除する場合、最も一般的で簡潔な方法は append 関数を使用することです。

append関数を利用した削除

この手法では、削除したい要素の前後を切り取り、それらを結合することで新しいスライスを作成します。

Go
package main

import "fmt"

func main() {
    // 削除対象のスライス
    fruits := []string{"apple", "banana", "cherry", "date", "elderberry"}
    
    // インデックス2(cherry)を削除する
    i := 2
    fruits = append(fruits[:i], fruits[i+1:]...)
    
    fmt.Println(fruits)
}
実行結果
[apple banana date elderberry]

このコードの fruits[:i] はインデックス0から i-1 までの要素を指します。

そして fruits[i+1:]... はインデックス i+1 から最後までの要素を展開して append に渡しています。

この方法は非常に直感的であり、スライスの要素の順序を維持できるという大きなメリットがあります。

copy関数を利用した削除

大規模なスライスでパフォーマンスを微調整したい場合、 copy 関数を使用することもあります。

Go
package main

import "fmt"

func main() {
    s := []int{10, 20, 30, 40, 50}
    i := 1 // 20を削除
    
    // 後続の要素を1つ前にずらす
    copy(s[i:], s[i+1:])
    
    // スライスの末尾を切り落とす
    s = s[:len(s)-1]
    
    fmt.Println(s)
}
実行結果
[10 30 40 50]

copy 関数を使用すると、メモリの移動が明示的になります。

ただし、通常の用途であれば append を使用した方がコードが短く読みやすいため、一般的には append が推奨されます。

順序を保持しない高速な削除テクニック

スライス内の要素の順序が重要ではない場合、計算量を抑えた非常に高速な削除方法が存在します。

末尾要素との入れ替え

削除したい要素をスライスの末尾にある要素で上書きし、スライスのサイズを1つ減らす手法です。

Go
package main

import "fmt"

func main() {
    nums := []int{1, 2, 3, 4, 5, 6}
    i := 2 // 値「3」を削除
    
    // 削除したい場所に末尾の要素をコピー
    nums[i] = nums[len(nums)-1]
    
    // 末尾を切り捨てる
    nums = nums[:len(nums)-1]
    
    fmt.Println(nums)
}
実行結果
[1 2 6 4 5]

この手法の最大の利点は、計算量が O(1) であることです。

appendcopy を使用する場合、削除位置より後ろの全要素をシフトさせる必要があるため、計算量は O(n) となります。

データ量が数万件を超えるような大規模なスライスで、かつ順序を問わない場合には、この高速削除テクニックが非常に有効です。

条件に一致する要素を複数削除するフィルタリング

特定の条件に合致する要素をすべて削除(あるいは残す)したい場合は、フィルタリングの考え方を用います。

インプレース・フィルタリング

新しいスライスを作成せずに、元のスライスのメモリ領域を再利用して効率的に処理する方法です。

Go
package main

import "fmt"

func main() {
    numbers := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
    
    // 偶数だけを残す(奇数を削除する)
    n := 0
    for _, x := range numbers {
        if x % 2 == 0 {
            numbers[n] = x
            n++
        }
    }
    numbers = numbers[:n]
    
    fmt.Println(numbers)
}
実行結果
[2 4 6 8 10]

このアルゴリズムは、条件を満たす要素だけをスライスの前方に詰めていく仕組みです。

追加のメモリ割り当てが発生しないため、非常にメモリ効率が良いのが特徴です。

slices.DeleteFunc を使用したモダンな記述

Go 1.21以降、標準ライブラリに slices パッケージが追加され、より宣言的な記述が可能になりました。

Go
package main

import (
    "fmt"
    "slices"
)

func main() {
    words := []string{"apple", "bat", "cherry", "dog", "egg"}
    
    // 3文字以下の単語を削除する
    words = slices.DeleteFunc(words, func(s string) bool {
        return len(s) <= 3
    })
    
    fmt.Println(words)
}
実行結果
[apple cherry]

slices.DeleteFunc を使うことで、ループ処理を自前で書く必要がなくなり、コードの意図が明確になります。

2026年現在の開発現場では、この slices パッケージの関数を利用するのが最も推奨されるスタイルです。

メモリリークを防ぐための重要な注意点

スライスの要素を削除する際、特に要素がポインタや構造体(ポインタを含むもの)である場合は、メモリ管理に注意が必要です。

ポインタ要素のガベージコレクションへの影響

スライスの長さを縮小しても、背後にある配列には元の要素への参照が残っている場合があります。

Go
// ポインタを含むスライスの削除例
s := []*SomeStruct{obj1, obj2, obj3}
i := 1

// 削除前に明示的にnilを代入して参照を切る
s[i] = nil 
s = append(s[:i], s[i+1:]...)

もし nil を代入せずにスライスの幅だけを狭めた場合、削除されたはずのオブジェクトがメモリ上に残り続け、ガベージコレクション(GC)によって解放されません。 これはメモリリークの原因となるため、長期間生存するスライスや、大きなオブジェクトを扱うスライスでは必ず意識すべきポイントです。

なお、前述の slices.Deleteslices.DeleteFunc は、内部的にこの nil 代入によるクリアを適切に処理してくれるため、安全に使用できます。

スライスの全要素を削除(クリア)する方法

スライスの容量を維持したまま、中身を空にしたい場合にはいくつかの方法があります。

スライスを再スライスして長さを0にする

最も一般的なのは、インデックス指定で長さを0にすることです。

Go
s := []int{1, 2, 3}
s = s[:0]

これにより、 len は0になりますが、 cap (容量)は維持されるため、その後の append 操作でメモリ再割り当てを抑制できます。

clear関数の使用

Go 1.21で導入された組み込み関数 clear を使用することもできます。

Go
s := []int{1, 2, 3}
clear(s)

ただし、 clear 関数をスライスに対して使用すると、すべての要素がその型のゼロ値で埋められるだけで、スライスの長さ自体は変わりません。

スライスの要素を完全に「除去」して長さを0にしたい場合は s = s[:0] を使用し、中身をゼロ値で初期化したい場合は clear(s) を使用するという使い分けが必要です。

パターン別:スライス削除手法の比較表

ここまで紹介した手法の特性を、以下の表にまとめました。

手法計算量順序維持主な用途
append による削除O(n)あり最も一般的。少〜中規模データの削除。
末尾入れ替え法O(1)なし大規模データで順序が不要な場合の高速化。
インプレース・フィルタO(n)あり条件に合う要素を一度に複数削除する場合。
slices.DeleteFuncO(n)ありモダンな記述。条件指定による削除の標準。
s = s[:0]O(1)容量を維持したまま全削除(クリア)。

用途に応じて最適な手法を選択することで、コードの可読性と実行効率を両立させることができます。

スライス操作におけるパフォーマンスの最適化ヒント

スライスの削除操作が頻繁に発生するアプリケーションでは、以下の点に留意するとさらにパフォーマンスが向上します。

1. 事前のアロケーション

削除後に新しいスライスを作る必要がある場合は、 make([]T, 0, originalCap) のように適切な容量を確保してください。

2. 不要なコピーの回避

スライスは参照型に近い構造体であるため、関数に渡す際はポインタを渡す必要は必ずしもありませんが、スライスのヘッダー(ポインタ、長さ、容量)自体が書き換わる削除操作を関数化する場合は、ポインタで渡すか、新しいスライスを戻り値として返す必要があります。

3. slicesパッケージの活用

slices パッケージはジェネリクスを用いて実装されており、型安全かつ最適化されたコードが提供されています。

独自のループを記述するよりも、可能な限り標準ライブラリに任せる方が、将来的なGoのアップデートによる最適化の恩恵を受けやすくなります。

まとめ

Go言語におけるスライスの要素削除は、言語の特性を理解する上で非常に良い例題となります。

標準的な append を使った方法から、 slices パッケージによるモダンな手法、そして計算量を抑えるための末尾入れ替えテクニックなど、状況に応じて最適な手段は異なります。

特に以下の3点は常に意識しておきましょう。

  • 順序を維持する必要があるか。
  • メモリ効率(ポインタの処理)に問題はないか。
  • 2026年現在の標準である slices パッケージを優先的に活用できているか。

これらのテクニックを使い分けることで、バグが少なく、かつパフォーマンスに優れたGoプログラムを記述できるようになります。

日常的な開発の中で、今回紹介した各パターンのメリット・デメリットを思い出しながら、最適な実装を選択してください。