Go言語の開発において、動的なデータ構造である「map」は非常に頻繁に使用される便利な機能です。

プログラムの実行中に特定のキーと値のペアが不要になった場合、メモリを適切に管理し、意図しない挙動を防ぐために要素を削除する必要があります。

Go言語には、mapから要素を取り除くための組み込み関数としてdeleteが用意されています。

本記事では、このdelete関数の基本的な使い方から、実務で直面しやすい注意点、さらには最新のGoバージョンで導入された便利な機能まで詳しく解説します。

Go言語のmapにおける要素削除の基本

Go言語でmapから特定のキーを持つ要素を削除するには、組み込みのdelete関数を使用します。

この関数は、第一引数に操作対象のmapを、第二引数に削除したい要素のキーを指定します。

Go
package main

import "fmt"

func main() {
    // mapの初期化
    fruits := map[string]int{
        "apple":  100,
        "banana": 200,
        "orange": 300,
    }

    fmt.Println("削除前:", fruits)

    // "banana"キーの要素を削除
    delete(fruits, "banana")

    fmt.Println("削除後:", fruits)
}
実行結果
削除前: map[apple:100 banana:200 orange:300]
削除後: map[apple:100 orange:300]

上記のコードでは、delete(fruits, "banana")を実行することで、mapから「banana」というキーとその値が完全に取り除かれています。

この関数は値を返さないため、実行結果を代入することはできません

存在しないキーを指定した場合の挙動

delete関数に、対象のmapに含まれていないキーを指定した場合はどうなるのでしょうか。

結論から述べると、存在しないキーを指定してもエラーやパニックは発生しません。 プログラムはそのまま続行され、mapの状態も変化しないだけで済みます。

そのため、削除前にわざわざ「キーが存在するかどうか」をチェックする必要はなく、安全に記述することができます。

nilのmapに対してdeleteを実行した場合

Go言語では、初期化されていないmap(nil map)に対して書き込みを行うとパニックが発生します。

しかし、delete関数をnilのmapに対して実行した場合はパニックになりません

単に何も処理が行われない(no-op)という挙動になります。

ただし、map自体がnilである状態で他の操作を行うと危険であるため、基本的には適切に初期化されたmapに対して操作を行うのがベストプラクティスです。

繰り返し処理(for range)中の要素削除

Go言語のmapにおいて非常に強力な特性の一つが、「イテレーション(繰り返し処理)の最中に要素を削除しても安全である」という点です。

他のプログラミング言語では、ループの中でコレクションの内容を変更すると実行時エラーが発生することがありますが、Go言語では公式にサポートされています。

Go
package main

import "fmt"

func main() {
    scores := map[string]int{
        "Alice": 95,
        "Bob":   60,
        "Charlie": 80,
        "David": 55,
    }

    // 70点未満の要素をループ中に削除
    for name, score := range scores {
        if score < 70 {
            delete(scores, name)
        }
    }

    fmt.Println("選考通過者:", scores)
}
実行結果
選考通過者: map[Alice:95 Charlie:80]

このように、条件に合致した要素をその場で削除しても、イテレータが壊れることはありません。

ただし、ループ中に「新しい要素を追加」した場合は、その要素がループ内で処理されるかどうかが保証されないため注意が必要です。

Go 1.21で導入されたclear関数による全削除

これまでは、mapのすべての要素を削除して中身を空にする際、ループで一つずつ削除するか、新しい空のmapを再代入する方法が一般的でした。

しかし、Go 1.21からは新しく導入されたclear関数を使用することが推奨されています。

clear関数を使用すると、mapの型や容量を維持したまま、中身の要素だけを効率的に一括削除できます。

Go
package main

import "fmt"

func main() {
    data := map[int]string{1: "A", 2: "B", 3: "C"}

    fmt.Println("クリア前:", data)

    // 全要素を削除
    clear(data)

    fmt.Println("クリア後:", data)
    fmt.Println("長さ:", len(data))
}
実行結果
クリア前: map[1:A 2:B 3:C]
クリア後: map[]
長さ: 0

clear関数を使用するメリットは、ループを記述する手間が省けることと、後述するメモリの再利用効率が向上することにあります。

mapの要素削除とメモリ管理の注意点

mapから要素を削除する際に、エンジニアが最も注意しなければならないのが「メモリの解放タイミング」です。

実は、delete関数を使用して要素を一つずつ削除しても、そのmapが占有しているメモリ領域(バケット)はすぐにはOSに返却されません

メモリが縮小しない現象

Goのmapは、ハッシュテーブルの内部構造として「バケット」という単位でデータを管理しています。

要素を削除しても、一度確保されたバケットは再利用のために保持され続けます。

つまり、巨大なmapからほとんどの要素を削除しても、メモリの使用量は高いまま維持される可能性があります。

この挙動を理解していないと、長期間稼働するプロセスにおいてメモリリークのような症状に見舞われることがあります。

メモリを確実に解放する方法

メモリを完全に解放し、フットプリントを最小限に抑えたい場合は、新しいmapを作成して古いmapを破棄する手法をとります。

Go
// メモリをリセットする例
m := make(map[int]string, 1000000)
// ... 大量にデータを入れる ...

// 削除してもメモリは減らない
for k := range m {
    delete(m, k)
}

// 完全にメモリを解放したい場合は再代入する
m = nil // または m = make(map[int]string)

このように、参照を切り替えることで古いmapはガベージコレクション(GC)の対象となり、メモリが物理的に解放されます。

頻繁に巨大なmapをクリアして再利用する場合は、clear関数を使うことでバケット自体を再利用できるため、アロケーションのコストを抑えることができます。

並行処理におけるmap削除の危険性

Go言語の標準のmapは、スレッドセーフ(並行安全)ではありません

複数のゴルーチンから同時に同じmapに対してdelete操作を行ったり、読み取りと削除を同時に行ったりすると、プログラムがクラッシュ(ランタイムパニック)します。

これを回避するためには、sync.Mutexなどの排他制御メカニズムを使用する必要があります。

Mutexを用いた安全な削除操作

以下は、複数のゴルーチンから安全に要素を削除する実装例です。

Go
package main

import (
    "sync"
)

type SafeMap struct {
    mu sync.Mutex
    data map[string]int
}

func (s *SafeMap) Remove(key string) {
    s.mu.Lock()
    defer s.mu.Unlock()
    delete(s.data, key)
}

func main() {
    sm := &SafeMap{
        data: make(map[string]int),
    }
    
    // 安全に削除を実行
    sm.Remove("example")
}

もし、高度な並行処理の中で頻繁にキーの追加と削除が発生する場合は、標準ライブラリのsync.Mapの使用も検討してください。

ただし、sync.Mapは特殊なユースケースに最適化されているため、通常のアプリケーションコードではMutexと標準のmapを組み合わせる方がパフォーマンスが良い場合が多いです。

まとめ

Go言語のmapにおける要素削除は、組み込みのdelete関数を用いることで非常にシンプルに行うことができます。

存在しないキーやnilのmapに対しても安全に動作する点は、エラーハンドリングを簡略化できる大きなメリットです。

また、ループ中の削除が可能であるという柔軟性を持ちつつ、Go 1.21以降はclear関数によって効率的な一括削除も可能になりました。

一方で、メモリ管理の側面では「削除しても即座にメモリが返却されない」という特性を理解しておくことが、大規模なシステム開発では重要になります。

今回紹介した基本的な使い方と、メモリや並行処理に関する注意点を踏まえ、より安全で効率的なGoプログラムを記述していきましょう。