Deleting Entries from Maps

How to remove entries from Go maps using the delete and clear built-in functions, including iteration-based clearing, tombstones, memory implications, and concurrency safety.

Removing entries from a map is a common operation, and Go provides two built-in functions for it: delete for individual keys and clear (from Go 1.21 onward) to empty an entire map. Both are designed to be safe and predictable, but the way the map data structure handles removal behind the scenes – using tombstones – is worth understanding because it affects memory usage and lookup correctness.

The delete Built-in

delete removes a single key‑value pair from a map. The key does not need to exist, and the map may even be nil. In both cases, delete simply does nothing.

delete(m, key)
  • m is the map you want to modify.
  • key is the specific key whose entry should be removed.

The function has no return value. If the map is nil, the call is a no‑op and does not panic. This is intentionally different from writing to a nil map, which causes a runtime panic.

Nil map writes vs. deletes:

Assigning a new key to a nil map panics with assignment to entry in nil map, but calling delete on a nil map is perfectly safe. Keep this asymmetry in mind so you don't assume a nil map is always harmless.

scores := map[string]int{
    "alice": 92,
    "bob":   85,
    "carol": 78,
}
delete(scores, "bob")
delete(scores, "dave") // key does not exist – nothing happens
fmt.Println(scores) // map[alice:92 carol:78]

The first delete call removes "bob". The second targets a key that was never in the map. Both work without error. The output confirms that "bob" is gone and the rest of the map is untouched.

Beginners often worry about calling delete with a missing key. There is no need to guard it with a comma-ok check – the built-in is defined to be idempotent, so repeated deletes of the same key behave identically to a single call.

Irreflexive Keys and NaN

A subtle limitation of delete appears with keys that are not equal to themselves – specifically, math.NaN() floating‑point values and composite types containing a NaN. Because NaN != NaN, there is no way to construct a key that matches an existing NaN entry using delete(m, key).

m := map[float64]string{}
m[math.NaN()] = "payload"
delete(m, math.NaN()) // cannot find the key – no-op
fmt.Println(len(m)) // 1 – the NaN entry remains

The map still holds the entry after the delete call. The built-in clear function (Go 1.21+) solves this because it iterates over the map and removes each entry directly, using the exact key stored in the bucket, so NaN keys are cleared as expected.

NaN keys are invisible to delete:

If you store data under a NaN key, you cannot remove that specific entry with delete. The key can only be removed by clearing the entire map with clear or by iterating and deleting with the range variable.

Deleting During Range Iteration

Go’s specification explicitly allows deleting map entries while iterating with for range. The iteration is based on a snapshot of the map’s state before the loop begins, and the rules are well‑defined:

  • Entries that exist when the loop starts will be visited exactly once, unless they are deleted before the loop reaches them.
  • If a map entry that has not yet been reached is deleted during iteration, that entry will simply not be produced by the loop.
  • Deleting the current entry (the one being iterated) is also safe.

This makes patterns like delete items that match a condition straightforward and free of temporary lists.

products := map[string]float64{
    "apple":  0.99,
    "banana": 1.25,
    "cherry": 2.10,
    "date":   0.00, // discontinued
}
for name, price := range products {
    if price == 0.00 {
        delete(products, name)
    }
}
fmt.Println(products) // map[apple:0.99 banana:1.25 cherry:2.10]

The loop checks each product and removes the one with a zero price. The remaining entries are unaffected. Notice that "date" was reached by the range clause, yet deleting it right away is permitted.

Safe and clean filtering:

When you need to prune a map, ranging with delete is the canonical Go idiom. It avoids building a separate list of keys to remove after the loop, which you would be forced to do in many other languages.

Clearing All Entries

You have three main ways to clear every entry from a map, each with its own trade‑off.

m := map[string]int{"x": 1, "y": 2}
clear(m)
fmt.Println(len(m)) // 0

clear(m) deletes every entry from the map, including NaN keys, leaving len(m) == 0. It is a single, concise call and was introduced in Go 1.21.

The third option – assigning a new map with make – is not the same operation. Reassigning creates a fresh map and abandons the old one.

a := map[string]int{"hello": 1}
b := a                     // b points to the same underlying map
a = make(map[string]int)  // a now points to a new, empty map
fmt.Println(b)            // map[hello:1] – b still sees the original data

If any other variable or goroutine holds a reference to the original map, that reference will still see the old data. Choosing between clear / for delete and reassignment depends on whether you want to mutate the shared map or detach from it.

Memory and Tombstones – What Happens Internally

When you delete a key, the map runtime does not immediately shrink the underlying bucket array or free the memory that stored the entry. Instead, it marks the slot with a special “tombstone” marker. A tombstone is not a zero value; it tells the map that a slot was once occupied so that lookups for other keys that hash to the same bucket chain can continue scanning past it without stopping.

This design solves a critical problem. Without tombstones, a lookup for a key could encounter an empty slot, assume the key does not exist, and stop searching – even if the key lives in a later overflow bucket. By marking deleted slots, the map preserves the integrity of the probe sequence.

Tombstones also serve as reusable slots. When a new key is later inserted into the same bucket chain, the runtime will repurpose a tombstone before allocating an overflow bucket. This recycling helps keep lookup chains short and avoids premature growth.

Memory occupied by the deleted key and value is only reclaimed when:

  • The slot is overwritten by a new insertion, or
  • The map grows and rehashes all live entries, discarding the tombstones entirely.

This is why a map’s memory footprint does not drop after a series of deletes – it stays allocated until a growth event redistributes the remaining keys.

For most programs, this behavior is invisible and benign. The map remains fast because tombstones keep lookups correct, and memory tends to stabilize as slots are reused.

Concurrency Safety

Maps are not safe for concurrent use. If one goroutine calls delete (or any other mutation) while another goroutine reads or iterates, the result is a data race. The runtime may even detect this and crash with a fatal error.

To coordinate access, use a sync.Mutex, sync.RWMutex, or the sync.Map type – but only when your workload truly benefits from its specific concurrency semantics.

No implicit concurrency protection:

Concurrent map writes – including delete – without explicit synchronization lead to undefined behavior. Use go test -race to detect these races during development.

Summary

Deleting entries is a fundamental map operation that Go makes safe and simple. delete removes a single key without complaining, and clear empties the whole map in one call starting in Go 1.21. Iterating and deleting is well‑defined, letting you filter maps in place. The internal tombstone mechanism keeps lookups accurate, though it means memory is not released immediately. When concurrency is involved, always add synchronization – the map itself provides none.