Removing map entries by a condition is common when cleaning caches, pruning stale state, or dropping records that no longer belong in a working set. A for range loop with delete works, but when the operation is simply “delete every entry matching this predicate,” maps.DeleteFunc states that intent directly.
The function mutates the map you pass to it. That makes it a good fit for owned, mutable state, but a poor fit when callers still need the original contents.
How maps.DeleteFunc filters a Go map
maps.DeleteFunc is part of Go’s maps package:
func DeleteFunc[M ~map[K]V, K comparable, V any](m M, del func(K, V) bool)It visits map entries and calls del with both the key and value. If the predicate returns true, that entry is deleted. If it returns false, the entry remains.
That true means delete rule is worth keeping visible in the predicate. Code written with filter-style APIs often uses the opposite convention, where true means keep.
Suppose a service tracks sessions and periodically removes inactive ones:
package main
import (
"fmt"
"maps"
)
type Session struct {
User string
Active bool
}
func main() {
sessions := map[string]Session{
"s-101": {User: "ana", Active: true},
"s-102": {User: "bo", Active: false},
"s-103": {User: "cy", Active: false},
}
maps.DeleteFunc(sessions, func(id string, s Session) bool {
return !s.Active
})
fmt.Println(len(sessions))
fmt.Println(sessions["s-101"].User)
}The resulting map contains only s-101. There is no replacement map to assign because maps.DeleteFunc changes sessions directly and returns no value.
Use both the key and value when the rule needs them
A hand-written cleanup loop often exists because the deletion rule isn’t based on the value alone. maps.DeleteFunc passes both parts of each entry, so key-aware rules don’t need an extra lookup.
For example, imagine configuration values where temporary keys use a tmp: prefix, but only disabled temporary entries should be removed:
maps.DeleteFunc(config, func(key string, enabled bool) bool {
return strings.HasPrefix(key, "tmp:") && !enabled
})The predicate captures the whole deletion rule in one place. A permanent disabled entry stays, and an enabled temporary entry stays. Only entries satisfying both conditions are removed.
Keep predicates side-effect-free when practical. A predicate that also updates unrelated application state can make cleanup behavior difficult to reason about, especially because Go map iteration order isn’t specified. The predicate should usually answer one question: should this key-value pair be deleted?
maps.DeleteFunc versus a for-range loop
There is nothing wrong with deleting entries during a normal map range loop. This is clear Go:
for key, value := range sessions {
if !value.Active {
delete(sessions, key)
}
}maps.DeleteFunc is useful when the loop has no other job. It removes the mechanics and leaves the condition:
maps.DeleteFunc(sessions, func(_ string, value Session) bool {
return !value.Active
})Prefer the explicit loop when you also need to count removals, emit an audit record, accumulate deleted values, or handle different cases separately. maps.DeleteFunc doesn’t report how many entries it removed and doesn’t return the deleted entries. You can add side effects inside the predicate, but doing so often hides work that would be clearer in a loop.
This is a readability choice rather than a rule that every conditional deletion should use the helper.
Preserve the original map by cloning first
Because maps.DeleteFunc mutates its argument, filtering a map that is shared as an input can produce surprising changes elsewhere in the program. Map assignment doesn’t make an independent copy of the entries:
filtered := original
maps.DeleteFunc(filtered, shouldDelete)Here, deleting through filtered also changes original because both variables refer to the same map.
When you need a filtered copy, clone first:
filtered := maps.Clone(original)
maps.DeleteFunc(filtered, shouldDelete)maps.Clone is a shallow clone. The map itself is separate, but values that contain pointers, slices, maps, or other reference-like data can still refer to shared underlying data. For a deletion-only operation that’s often exactly what you need: the set of keys can diverge without copying every object stored as a value.
Nil and empty maps need no special branch
A nil map has no entries to visit, so maps.DeleteFunc has nothing to delete. Calling it with a nil map is safe:
var counts map[string]int
maps.DeleteFunc(counts, func(_ string, count int) bool {
return count == 0
})
fmt.Println(counts == nil) // trueThe map remains nil. An allocated but empty map likewise remains empty.
This can simplify cleanup code that accepts optional map state. You don’t need an if m != nil guard just to call maps.DeleteFunc.
Watch for mutation and concurrency boundaries
maps.DeleteFunc is an in-place operation, so ownership matters. If a map is treated as immutable after publication, filtering it directly breaks that contract even if the helper makes the code concise. Clone it or construct a new map instead.
The helper also doesn’t make ordinary Go maps safe for concurrent access. If other goroutines may read or write the same map while cleanup runs, synchronization still belongs around the shared state. maps.DeleteFunc changes how the deletion loop is expressed; it doesn’t change the map’s concurrency model.
One more subtle mistake is assuming a predictable visitation order. Don’t write a predicate whose correctness depends on seeing one key before another. Map iteration order is unspecified, and a deletion predicate should not rely on it.
Choose the helper when deletion is the whole operation
maps.DeleteFunc works best when you already have a mutable map and the task is narrowly defined: remove every entry for which a key-value predicate returns true. It handles value-only and key-aware conditions without a manual deletion loop.
If you need the original map afterward, clone before deleting. If deletion is only one part of a larger pass, keep the for range loop so the extra work stays visible. That boundary keeps maps.DeleteFunc useful without forcing a compact helper onto code that has more to say.