The maps Standard Library Package
A complete guide to the maps package in Go - generic functions for cloning, comparing, filtering, and iterating over maps without manual boilerplate
Why the maps Package Exists
Before generics arrived in Go 1.18, writing reusable functions for maps meant either hard-coding a specific key-value type or using reflection, which is slow and unsafe. You ended up writing the same loop to collect keys or copy a map in every project.
Go 1.18 made it possible to write type-safe generic container functions. The experimental module golang.org/x/exp/maps was the testing ground. Once the design proved itself, the package moved into the standard library as maps in Go 1.21, and gained iterator-based functions in Go 1.23. It replaces dozens of manual for loops with short, expressive calls that the compiler can check.
Think of the maps package as a small toolbox. It does not try to turn Go into a functional programming language. It gives you the operations that appear so often in real codebases that writing them by hand becomes noise.
Functions at a Glance
| Function | Purpose |
|---|---|
Clone | Create a shallow copy of a map |
Copy | Copy all entries from one map into another |
DeleteFunc | Remove every entry that satisfies a predicate |
Equal | Compare two maps for equality using == on values |
EqualFunc | Compare two maps using a custom equivalence function |
Keys | Return an iterator over all keys (Go 1.23+) |
Values | Return an iterator over all values (Go 1.23+) |
All | Return an iterator over key-value pairs (Go 1.23+) |
Iterator functions are Go 1.23+:
Keys, Values, and All were added in Go 1.23 alongside the language support for range-over-function iterators. If you are using an older version, these three functions will not be available. The rest of the package works from Go 1.21 onward.
Iterating with Keys, Values, and All
Go 1.23 introduced a new kind of range loop that works with iterator functions — functions that accept a yield callback. The maps package exposes three of these.
maps.Keys(m) returns an iterator over the keys of m. You use it directly in a range clause.
package main
import (
"fmt"
"maps"
)
func main() {
population := map[string]int{
"Delhi": 32_000_000,
"Shanghai": 24_000_000,
"São Paulo": 22_000_000,
}
for city := range maps.Keys(population) {
fmt.Println(city)
}
}
This prints each city once, in an unpredictable order — exactly like for k := range population. The advantage appears when you pass the iterator to another function instead of looping immediately.
maps.Values(m) returns an iterator over the values. maps.All(m) returns an iterator over both key and value pairs, so for k, v := range maps.All(m) works identically to for k, v := range m. The real value of all three functions is composition: you can feed them into functions like slices.Collect, slices.Sorted, or your own iterator-consuming helpers.
A common question from developers moving from x/exp/maps is: where did the slice go? The experimental x/exp/maps.Keys returned []K. The standard library version returns an iterator. To get a slice, wrap it with slices.Collect.
import (
"maps"
"slices"
)
keys := slices.Collect(maps.Keys(population))
Iterator functions do not return slices:
Calling maps.Keys(m) alone does not give you a []K. It returns an iter.Seq[K]. If you assign the result to a variable and try to index it like result[0], the code will not compile. Use slices.Collect to materialise the slice when you need one.
This design avoids allocating a slice when all you want is to range over the keys and maybe break early. For a map with millions of entries, skipping the allocation is a measurable win.
Cloning and Copying
maps.Clone(m) creates a shallow copy. The new map has the same keys and values, but it is a distinct map — adding or removing entries in the copy does not affect the original.
original := map[string]int{"a": 1, "b": 2}
clone := maps.Clone(original)
clone["c"] = 3
fmt.Println(original) // map[a:1 b:2]
fmt.Println(clone) // map[a:1 b:2 c:3]
The copy is shallow. If the values are pointers, slices, or other reference types, both maps share the underlying data. Modifying the pointed-to object through one map will be visible through the other. For value types like int, string, or structs that contain only value fields, the copy is fully independent.
maps.Copy(dst, src) copies every key-value pair from src into dst. If a key already exists in dst, its value is overwritten. This mirrors the behaviour of the built-in copy for slices.
dst := map[string]int{"x": 10}
src := map[string]int{"y": 20, "z": 30}
maps.Copy(dst, src)
// dst is now map[x:10 y:20 z:30]
Copy is a single function call that replaces the for k, v := range src { dst[k] = v } loop you would otherwise write.
Nil destinations are safe:
If you call maps.Copy(dst, src) and dst is nil, you get a panic — just as you would when assigning to a nil map. Always initialise the destination map with make before copying. If src is nil, Copy does nothing and does not panic.
Comparing Maps for Equality
maps.Equal(m1, m2) returns true if the two maps have the same length and every key in one map exists in the other with the same value, compared using ==. This means the value type must be comparable. For maps with slices or other non-comparable values, Equal will not compile.
a := map[int]string{1: "one", 2: "two"}
b := map[int]string{2: "two", 1: "one"}
fmt.Println(maps.Equal(a, b)) // true
Order does not matter. Two nil maps are considered equal. An empty map and a nil map are also equal because they have the same length (zero) and contain no keys.
When the value type is not comparable — for example, a slice — use maps.EqualFunc. It accepts a function that compares two values and returns a boolean.
m1 := map[string][]int{"a": {1, 2}}
m2 := map[string][]int{"a": {1, 2}}
equal := maps.EqualFunc(m1, m2, func(s1, s2 []int) bool {
return slices.Equal(s1, s2)
})
fmt.Println(equal) // true
You provide the equality logic. The function is called only for keys that exist in both maps, and only once per key.
Equal on nil vs empty map:
maps.Equal(nil, map[string]int{}) returns true. This is consistent with len(nil map) == 0, but it can hide a bug where one variable is unexpectedly nil. If your program distinguishes between a nil map and an initialized empty map, you need an explicit nil check before calling Equal.
Deleting Entries by Predicate
maps.DeleteFunc(m, func(key K, value V) bool) removes every entry for which the predicate returns true. It modifies the map in place.
scores := map[string]int{
"Alice": 95,
"Bob": 42,
"Carol": 78,
}
maps.DeleteFunc(scores, func(name string, score int) bool {
return score < 50
})
// scores is now map[Alice:95 Carol:78]
The predicate receives both the key and the value. You decide which entries to remove based on either. The function iterates over the map and deletes matching keys; the map shrinks accordingly.
Do not delete inside a range loop:
DeleteFunc is the safe, standard-library way to delete matching entries. If you write for k, v := range m { if cond { delete(m, k) } }, the behaviour is safe in Go (the language specification guarantees that deleting during a range is safe), but DeleteFunc expresses the intent more clearly and avoids the temptation to add additional logic inside the loop that could cause issues.
Common Patterns
The iterator functions combine cleanly with the slices package to produce concise data pipelines.
Sorted keys for deterministic output:
sortedKeys := slices.Sorted(maps.Keys(userAges))
for _, name := range sortedKeys {
fmt.Println(name, userAges[name])
}
Collecting values that meet a condition:
You can write a small filter iterator (or use slices.Collect after filtering manually), but a simple loop is often clearer. However, if you already have a filtering helper, you can chain it.
highScores := slices.Collect(filterValues(maps.All(scores), func(name string, s int) bool {
return s > 70
}))
The maps package intentionally stops short of providing Filter or Map functions. The Go team decided that those operations often obscure allocation costs and are overused. You can still build them with the building blocks when you need them, and a plain loop remains the recommended default.
Building a frequency counter from a slice:
words := []string{"go", "map", "go", "iterator"}
freq := make(map[string]int)
for _, w := range words {
freq[w]++
}
// No maps function needed here; the package does not replace basic loops.
The package shines when you need to pass map data to another algorithm without intermediate slices.
Migrating from x/exp/maps
The experimental package golang.org/x/exp/maps was the precursor. If your code still imports it, moving to the standard library is straightforward but requires a few deliberate changes.
Replace the import
Remove "golang.org/x/exp/maps" and add "maps". All the core functions (Clone, Copy, DeleteFunc, Equal, EqualFunc) have the same signatures and behaviour. Your existing calls to these will continue to compile.
import "maps"
Adapt Keys and Values calls
In x/exp/maps, Keys and Values returned slices. In the standard library, they return iterators. If you stored the slice in a variable, the code will not compile. You have two options:
- Use
slices.Collect(maps.Keys(m))to get the slice. This restores the old behaviour. - Rework the downstream code to accept an iterator, which avoids the slice allocation and is often cleaner.
// Old
// allKeys := maps.Keys(m)
// New, slice equivalent
allKeys := slices.Collect(maps.Keys(m))
Verify compilation and tests
Run go build and your test suite. Every function from x/exp/maps that exists in the standard library is a drop-in replacement for the equivalent operation. The only compilation breaks will be related to slice-vs-iterator expectations.
Migration complete:
If your project compiles and all tests pass after these steps, you have successfully moved from the experimental package to the standard library. The x/exp/maps import can be removed from go.mod entirely.
Important Considerations
Nil Maps
All maps functions handle nil maps safely for read-like operations. Clone(nil) returns nil. Equal(nil, nil) returns true. DeleteFunc(nil, ...) does nothing. But Copy(dst, src) will panic if dst is nil, because it must write to the destination map. Always initialise the target map before copying.
Iterator Invalidation
When you use maps.Keys, maps.Values, or maps.All, the iterator is a snapshot of the map at the moment the iteration begins? Actually, Go maps do not support concurrent modification, and the iterator reflects the map state as iteration progresses. Adding or deleting keys in the same goroutine while consuming the iterator can lead to skipped entries or duplicate keys, because the underlying map may reorder. This is the same behaviour as a for range loop. Do not modify the map inside the loop body that consumes the iterator, unless you are using DeleteFunc which is designed for that.
Comparison Performance
maps.Equal and maps.EqualFunc are O(n) in the number of entries. They do a length comparison early, so if lengths differ, they return false immediately. For large maps where equality checks are frequent, consider whether comparing hashes or version counters would be cheaper.
What the Package Does Not Include
You will not find Filter, Map, Reduce, or Transform functions. The Go standard library deliberately avoids them. If you find yourself writing chains of iterator transformations, a simple for loop is often more readable and makes allocations explicit. The maps package gives you the essentials that are genuinely painful to reimplement correctly — particularly equality with custom comparison and predicate-based deletion — and stays out of the way for everything else.
Summary
The maps package takes the handful of map operations that every Go developer writes by hand and gives them a single, correct implementation backed by the type system. Clone and Copy remove boilerplate copying loops. Equal and EqualFunc give you safe map comparison without ordering gotchas. DeleteFunc is the canonical way to prune a map. Keys, Values, and All — added in Go 1.23 — turn map iteration into composable building blocks that integrate with the rest of the standard library, especially slices.
The most important shift to internalise is that maps.Keys and maps.Values return iterators, not slices. When you need a slice, slices.Collect is one call away. When you don't, you avoid an allocation. This design rewards you for staying lazy about materialising data until the last possible moment.