The maps package, added in Go 1.21 and expanded in Go 1.23, gives you a small set of generic functions that work on any map[K]V. Before it existed, every Go project carried its own handwritten copy of cloneMap, mergeMap, and mapKeys, all subtly different. The package replaces those with a tested, type-safe API and integrates with the new iter sequences so you can stream entries instead of materializing slices.
Before Go 1.18, you couldn't write a function that worked on map[string]int and map[int]Product at the same time without interface{} and reflection. So every codebase grew its own copies of the same handful of helpers, each typed for one specific map and copy-pasted into the next one.
Generics fixed the language gap, but the standard library still didn't ship the helpers. People kept writing them. Go 1.21 finally added maps and slices together to put the most common patterns in one tested place.
That one line, maps.Clone(inventory), used to be a five-line loop in every project that needed a snapshot. The package collects this and a dozen similar patterns under a single import.
The package splits into two groups based on Go version:
| Function | Added in | What it does |
|---|---|---|
Clone | 1.21 | Returns a shallow copy of the map |
Copy | 1.21 | Copies one map's entries into another |
Equal | 1.21 | Reports whether two maps have the same key-value pairs |
EqualFunc | 1.21 | Same as Equal with a custom value comparator |
DeleteFunc | 1.21 | Deletes entries that satisfy a predicate, in place |
Keys | 1.23 | Returns an iter.Seq[K] over the map's keys |
Values | 1.23 | Returns an iter.Seq[V] over the map's values |
All | 1.23 | Returns an iter.Seq2[K, V] over key-value pairs |
Collect | 1.23 | Builds a map[K]V from an iter.Seq2[K, V] |
Insert | 1.23 | Inserts entries from an iter.Seq2 into an existing map |
The 1.21 functions operate on whole maps. The 1.23 additions plug into the iter package so you can compose maps with slices, filter pipelines, and your own range-over-func loops.
The two groups answer different questions. The 1.21 group answers "I have a map, what can I do to it?" The 1.23 group answers "I have a stream of pairs, how do I get them in or out of a map?" Most real code uses both: clone a config, iterate the keys, build a derived map from a filter pipeline.
These four cover the most common map-level operations. They were the first batch shipped in Go 1.21, and they're enough to retire most handwritten map helpers.
Clone allocates a new map with the same key and value types and copies every entry from the source. It's a shallow copy, the keys and values are copied by assignment, not deep-cloned.
Clone preallocates a map sized for the source, so it's noticeably faster than the equivalent loop. If m is nil, Clone returns nil rather than an empty map, which preserves the nil semantics for callers that distinguish the two.
Clone runs in O(n) but with one preallocated bucket array, so it avoids the rehash churn a for k, v := range src { dst[k] = v } loop hits as the destination grows.
Copy is Clone's sibling for the case where the destination already exists. It writes entries from src into dst, overwriting any existing keys.
The pen price was overwritten, eraser was added, and notebook was left untouched. Copy has no return value because it mutates dst in place.
This makes Copy the standard tool for merging a partial update into a base map. It's also the building block for any "default config + overrides" pattern: clone the defaults, then Copy the user's overrides on top.
Equal reports whether two maps have the same key set and the same value for each key. It uses == to compare values, so it works only for maps whose value type is comparable.
Order doesn't matter, only the contents. Two nil maps compare equal. A nil map and an empty map also compare equal because both have zero entries.
If your values aren't comparable (slices, maps, functions, or structs containing them), Equal won't compile. That's where EqualFunc comes in. It lets you supply your own equality predicate.
The comparator runs once per shared key. If either map has a key the other doesn't, EqualFunc short-circuits to false without invoking the comparator.
DeleteFunc walks the map and removes every entry for which the predicate returns true. It modifies the map in place and returns nothing.
The predicate receives both the key and the value, so you can filter on either. Deleting from a map while ranging over it is safe in Go, so DeleteFunc doesn't need to build a separate list of keys-to-delete. It just iterates once and calls the runtime's delete primitive when the predicate fires.
Compare this with the equivalent handwritten loop:
The handwritten version works fine. DeleteFunc is the same thing wrapped in a function that's easier to read and harder to get wrong (no off-by-one, no captured variable bugs in nested loops).
DeleteFunc is in place. It doesn't allocate a new map. If you want a filtered copy without disturbing the original, clone first, then call DeleteFunc on the clone.
A pattern that comes up often: snapshot a config, then prune entries based on a runtime decision.
The original discountCodes map still has all five codes. active is the filtered view. This pairs Clone and DeleteFunc to build any "filtered copy" operation in two lines.
The diagram traces what happens to the data. Clone produces an independent map, then DeleteFunc shrinks it. The original on the left never changes, which is what makes this pattern safe to use on a shared config that other goroutines or callers might still read.
Go 1.23 added range-over-func and the iter package, and the maps package grew five functions that produce or consume those iterators. The two relevant types from iter are:
iter.Seq[V] is a sequence of single values.iter.Seq2[K, V] is a sequence of key-value pairs.Both are just function types you can range over with for v := range seq or for k, v := range seq.
maps.Keys(m) returns an iter.Seq[K] over the map's keys. maps.Values(m) returns an iter.Seq[V] over the values. Neither materializes a slice. They yield each entry once as you iterate.
Iteration order is still random, the same as a plain range over a map. maps.Keys doesn't sort. If you need sorted output, collect into a slice and sort it yourself, which is exactly what the example does.
The reason this exists at all is that an iter.Seq is composable. You can pass it to slices.Sorted, slices.Collect, slices.Max, or any other function that takes an iterator without materializing an intermediate slice.
slices.Sorted consumes an iterator and returns a sorted slice in one call. The old idiom for this was four lines: declare a slice, range over the map, append the keys, then sort.Strings. The iterator version reads like a single sentence.
slices.Max needs a slice, not an iterator, so the example threads Collect in to turn the sequence into a slice first. Mixing maps iterators with slices consumers is the design intent of the two packages.
maps.All(m) returns an iter.Seq2[K, V] over the whole map. It's the iterator form of a plain range m loop. It's most useful when you want to pass the map's entries to a function that takes an iter.Seq2, because you can't pass a map literal to such a function directly.
filter is a generic helper that takes an iter.Seq2 and returns a new iter.Seq2. It doesn't know it's working with a map. maps.All is the adapter that turns a map into the iterator the helper expects.
Collect goes the other way: it consumes an iter.Seq2[K, V] and builds a map[K]V. Insert does the same thing but writes into an existing map.
Collect doesn't know the final size in advance, so the destination map may grow and rehash as entries arrive. If you know roughly how many entries you'll get, building a make(map[K]V, n) and calling Insert is cheaper than Collect.
Collect allocates a fresh map. Insert mutates a map you already own, so it's the right choice for merging streamed data into an existing structure.
That last example does the same thing as maps.Copy(priceBook, updates). So why use Insert? Because the source doesn't have to be a map. It can be any iter.Seq2[K, V]: a database cursor wrapped in an iterator, a parser yielding key-value pairs, a filtered view of another map. Insert is the general form, Copy is the map-to-map shortcut.
Here's a fuller example that wires several iterator-aware functions together. The goal is to take an inventory map, keep only the products that are in stock, and turn the result into a price-indexed lookup map.
maps.All turns the source map into an iterator. filterStock returns a new iterator that yields only the in-stock entries. maps.Collect materializes the filtered stream into a map. slices.Sorted(maps.Keys(...)) gives a deterministic print order. Each step has one job, and none of them allocates a temporary slice or map for the in-between state.
The same handful of patterns show up over and over in code that uses maps. Worth seeing them spelled out.
A common need: capture the state of a shared configuration at a point in time so later mutations don't change the snapshot.
The snapshot still says new-checkout: true because Clone ran before the mutation. This is the pattern to use whenever you're returning a map from a method and you don't want the caller to be able to mutate your internal state.
maps.Copy overwrites on conflict. If you want different behavior, like keeping the existing value or taking the maximum, write the merge yourself.
maps.Copy would have overwritten notebook with 30 just because it appears later in warehouse2. mergeMax keeps the larger of the two, which is the right behavior when you're aggregating quantities from multiple sources.
When you need a sorted list of keys (for output, for batching, for deterministic iteration), the maps plus slices combination is hard to beat.
One line each. Compare with the pre-1.23 idiom of declaring a slice, ranging, appending, and sorting.
Clone is shallow. If your values are pointers, slices, or maps, both copies share the same inner data.
What's wrong with this code?
The output is [notebook pen], not [notebook]. Clone copied the *Order pointer into the new map, but both maps point to the same Order value. Mutating it through backup is visible through orders.
Fix:
You have to copy the nested data yourself. The standard library doesn't ship a generic deep-clone because deciding what counts as "deep" depends on your types.
Wrapping a map's iteration in maps.Keys or maps.All doesn't sort anything. The Go runtime intentionally randomizes map iteration order on every range start, and the iterators inherit that.
If you depend on a specific order, sort the result. The randomization is deliberate to keep programs from accidentally depending on a particular order.
Clone(nil) returns nil. Equal(nil, nil) returns true. Keys(nil) yields nothing. The read-side functions all handle nil maps gracefully, the same way a plain range over a nil map does (zero iterations, no panic).
Copy(nil, src) and Insert(nil, seq), on the other hand, panic. Writes to a nil map have always panicked in Go, and the helpers don't change that. Make sure the destination is initialized before calling either.
The takeaway is that read-side functions accept nil exactly the way the language does for plain range. The only operations that fail are the ones that would also fail on a raw map write.
EqualFunc takes a comparator that runs on values, not keys. The keys are still compared with ==. If your keys are something like file paths that need case-insensitive comparison on Windows, EqualFunc won't help. You'd need to normalize the keys before building the maps.
EqualFunc reported false even though the values agreed, because the keys "Notebook" and "notebook" are different strings under ==. The normalized version lowercases both sets of keys before comparing.
10 quizzes