An iterator is a convenient way to produce key-value pairs without deciding up front where they’ll be stored. Eventually, though, an API may need an ordinary map. Writing the collection loop yourself is straightforward, but Go 1.23 gives that boundary a standard name: maps.Collect.
maps.Collect consumes an iter.Seq2, stores each yielded pair in a newly allocated map, and returns that map. It’s a small helper, but it makes ownership clear: the sequence produces data; Collect creates the destination.
What maps.Collect does
The function has this signature:
func Collect[K comparable, V any](seq iter.Seq2[K, V]) map[K]VThe key type must be comparable, just as it must be for any Go map. Values can be any type. The input is an iter.Seq2[K, V], which is the two-value iterator shape used for key-value data.
Here’s a complete example:
package main
import (
"fmt"
"iter"
"maps"
)
func services(yield func(string, int) bool) {
entries := []struct {
name string
port int
}{
{"api", 8080},
{"metrics", 9090},
{"admin", 8081},
}
for _, entry := range entries {
if !yield(entry.name, entry.port) {
return
}
}
}
func main() {
ports := maps.Collect(iter.Seq2[string, int](services))
fmt.Println(ports["api"])
fmt.Println(ports["metrics"])
}The output is:
8080
9090There is no destination map to initialize and no assignment loop at the call site. maps.Collect owns that work.
If your producer already has the iter.Seq2[K, V] type, the explicit conversion used above isn’t necessary. It’s included to keep the example’s iterator function easy to read in isolation.
Collecting is the eager boundary
An iterator doesn’t require all of its values to exist in a collection at once. maps.Collect changes that. It consumes the sequence and materializes every yielded pair in a map before returning.
That makes it useful at boundaries where a map is genuinely required. For example, a configuration loader might lazily normalize entries and then hand a map to the rest of the application:
func normalized(entries iter.Seq2[string, string]) iter.Seq2[string, string] {
return func(yield func(string, string) bool) {
for key, value := range entries {
key = strings.TrimSpace(strings.ToLower(key))
value = strings.TrimSpace(value)
if key == "" {
continue
}
if !yield(key, value) {
return
}
}
}
}
settings := maps.Collect(normalized(source))The normalization remains iterator-based until the program actually needs indexed lookup by key. That separation is often cleaner than passing a partially built map through every transformation.
The trade-off is memory. Once you call maps.Collect, the result must hold the collected entries. If the sequence can be extremely large, unbounded, or more naturally processed one pair at a time, collecting it into a map may be the wrong operation.
Duplicate keys use normal map assignment semantics
A sequence can yield the same key more than once. A map can’t store multiple values under one key, so later pairs replace earlier ones.
package main
import (
"fmt"
"iter"
"maps"
)
func overrides(yield func(string, int) bool) {
pairs := []struct {
key string
value int
}{
{"timeout", 10},
{"retries", 2},
{"timeout", 30},
}
for _, pair := range pairs {
if !yield(pair.key, pair.value) {
return
}
}
}
func main() {
config := maps.Collect(iter.Seq2[string, int](overrides))
fmt.Println(config["timeout"])
fmt.Println(config["retries"])
}This prints:
30
2That behavior can be useful for ordered override streams, but it can also hide malformed input. If duplicate keys should be rejected rather than overwritten, maps.Collect doesn’t perform that validation for you. Put the duplicate check in the producer or use an explicit collection loop that can return an error.
For example, a parser that treats duplicate identifiers as invalid should detect them intentionally instead of relying on the resulting map to reveal the problem. By the time collection finishes, the overwritten value is gone.
Empty sequences produce an empty non-nil map
An empty iterator is a useful edge case because nil and empty maps behave similarly for reads but not identically in every context.
With maps.Collect, an empty sequence returns an empty, non-nil map:
package main
import (
"fmt"
"iter"
"maps"
)
func empty(yield func(string, int) bool) {}
func main() {
m := maps.Collect(iter.Seq2[string, int](empty))
fmt.Println(len(m))
fmt.Println(m == nil)
m["ready"] = 1
fmt.Println(m["ready"])
}The output is:
0
false
1The last assignment is the practical difference. Assigning to a nil map panics, while the empty map returned by maps.Collect is ready for writes.
Don’t build logic around nilness if what you mean is “contains no entries.” Use len(m) == 0 for that question. It works for both nil and non-nil empty maps and states the intent directly.
Build a map from indexed slice values
maps.Collect becomes more useful when combined with other iterator-aware standard-library functions. slices.All, for example, yields each slice index and value as a two-value sequence. That sequence can go straight into maps.Collect:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
names := []string{"api", "worker", "scheduler"}
byIndex := maps.Collect(slices.All(names))
fmt.Println(byIndex[0])
fmt.Println(byIndex[2])
}This produces a map[int]string whose keys are the original slice indexes.
That particular conversion isn’t something every program needs, but it demonstrates the composability of the iterator APIs. A function that returns iter.Seq2[K, V] doesn’t need to know whether its consumer will range over the pairs, insert them into an existing map, or collect them into a new one.
maps.Collect versus maps.Insert
maps.Collect and maps.Insert both consume an iter.Seq2, but they express different ownership decisions.
Use maps.Collect when the sequence should become a new map:
result := maps.Collect(pairs)Use maps.Insert when a map already exists and the sequence should update it:
result := map[string]int{
"existing": 1,
}
maps.Insert(result, pairs)The difference matters when defaults or previously accumulated state must survive. Collect starts with a fresh map, so there is nothing to preserve. Insert mutates the destination you provide.
If the source is already a map rather than an iterator, neither helper is necessarily the clearest choice. maps.Clone creates a shallow copy of a map, while maps.Copy copies entries into an existing destination. Converting a map to an iterator only to collect it again adds an unnecessary conceptual round trip.
Collection doesn’t make referenced values independent
A new map owns its key-to-value table, but ordinary Go assignment rules still apply to the values stored in it. If a yielded value contains a slice, map, pointer, or another reference-like value, collecting it doesn’t deep-copy the referenced data.
Consider a sequence that yields slices:
func groups(yield func(string, []int) bool) {
values := []int{1, 2, 3}
yield("primary", values)
}maps.Collect stores the slice value in the new map. The slice header is copied, but its backing array is still the same backing array supplied by the producer. If independent nested data is required, clone that data at the appropriate boundary.
This isn’t a special limitation of maps.Collect; it’s normal assignment behavior in Go. Still, it’s easy to read “new map” as “deep copy” when reviewing code quickly, so the distinction is worth making explicit.
Don’t assume collection preserves iteration order
Maps don’t represent insertion order. Even if the source sequence yields pairs in a deliberate order, the resulting map should be treated as unordered.
That means maps.Collect is a poor fit if later code needs to reproduce the sequence order. Keep a slice alongside the map, collect into a slice of records instead, or retain the iterator until the ordered processing is finished.
The same caution applies when the sequence itself comes from a map. Go map iteration order isn’t specified, and putting those entries into another map doesn’t create an ordering guarantee.
Keep validation before the collection boundary
maps.Collect is intentionally narrow: it turns key-value pairs into a map. It doesn’t validate domain rules, resolve errors, or decide whether overwriting a key is acceptable.
That makes the cleanest pipeline one where those decisions happen before collection. Normalize keys, reject invalid values, handle duplicate policy if necessary, and then collect once the sequence represents valid map entries.
Use maps.Collect when you have an iter.Seq2 and the next stage genuinely needs a new map. It removes a routine allocation-and-assignment loop while making the ownership transition visible in one expression. Keep the sequence lazy while that helps; collect at the point where keyed lookup becomes the operation you actually need.