Sometimes a Go pipeline naturally produces key-value pairs, but the destination is a map you already have. Turning those pairs into a temporary map just to merge it adds a step that doesn’t help.

Go 1.23 added maps.Insert for this case. It consumes an iter.Seq2[K, V] and writes each pair into an existing map. Existing keys are overwritten, unrelated entries stay in place, and the iterator can produce values lazily.

What maps.Insert changes

The function has a small API:

func Insert[Map ~map[K]V, K comparable, V any](m Map, seq iter.Seq2[K, V])

The first argument is the destination map. The second is an iterator that yields key-value pairs. maps.Insert ranges over that sequence and assigns every pair to the destination.

Here’s a complete example:

package main

import (
	"fmt"
	"maps"
	"slices"
)

func main() {
	names := map[int]string{
		99: "existing",
	}

	values := []string{"zero", "one", "two"}
	maps.Insert(names, slices.All(values))

	fmt.Println(names[0])
	fmt.Println(names[1])
	fmt.Println(names[2])
	fmt.Println(names[99])
}

slices.All(values) yields index-value pairs, so the map receives 0: "zero", 1: "one", and 2: "two". The entry at key 99 remains because the sequence never yields that key.

This is mutation, not construction. maps.Insert doesn’t return a map because it modifies the map passed to it.

Existing keys are overwritten

maps.Insert uses normal map assignment semantics. If the iterator yields a key that already exists, the new value replaces the old one.

package main

import (
	"fmt"
	"maps"
)

func main() {
	dst := map[string]int{
		"timeout": 10,
		"retries": 2,
	}

	overrides := map[string]int{
		"timeout": 30,
	}

	maps.Insert(dst, maps.All(overrides))

	fmt.Println(dst["timeout"]) // 30
	fmt.Println(dst["retries"]) // 2
}

That makes maps.Insert useful for overlay-style operations where later data is supposed to win. It also means you shouldn’t use it when duplicate keys are an error. If preserving the original value matters, put that rule in the sequence or handle insertion explicitly.

Duplicates can occur inside the sequence too. If it yields the same key more than once, each assignment replaces the previous value, so the last value actually yielded for that key remains in the map.

maps.Insert is useful when the source is already an iterator

If both source and destination are maps, maps.Copy(dst, src) is usually clearer. It says exactly what is happening and doesn’t require converting the source map to an iterator.

maps.Insert becomes more useful when the source is naturally an iter.Seq2. For example, you can filter records while yielding them instead of first building another collection.

package main

import (
	"fmt"
	"iter"
	"maps"
)

type Feature struct {
	Name    string
	Version int
	Enabled bool
}

func enabledFeatures(features []Feature) iter.Seq2[string, int] {
	return func(yield func(string, int) bool) {
		for _, feature := range features {
			if !feature.Enabled {
				continue
			}

			if !yield(feature.Name, feature.Version) {
				return
			}
		}
	}
}

func main() {
	features := []Feature{
		{Name: "search", Version: 2, Enabled: true},
		{Name: "billing", Version: 1, Enabled: false},
		{Name: "reports", Version: 3, Enabled: true},
	}

	versions := map[string]int{"legacy": 1}
	maps.Insert(versions, enabledFeatures(features))

	fmt.Println(versions["search"])  // 2
	fmt.Println(versions["reports"]) // 3
	fmt.Println(versions["legacy"])  // 1
	_, hasBilling := versions["billing"]
	fmt.Println(hasBilling) // false
}

The iterator owns the selection rule, while maps.Insert owns the mechanical work of consuming pairs and assigning them. No temporary map[string]int is needed between those two operations.

There’s a useful design boundary here. If the transformation is hard to understand when expressed as an iterator, a straightforward loop may still be better. maps.Insert removes collection plumbing; it doesn’t make complicated data logic simpler by itself.

Compose maps.All with iterator-aware code

Go 1.23 also added maps.All, which exposes a map as an iter.Seq2. That means maps.Insert(dst, maps.All(src)) is valid:

maps.Insert(dst, maps.All(src))

For a plain map-to-map copy, prefer:

maps.Copy(dst, src)

The iterator form earns its keep when another API already accepts or returns iter.Seq2, or when you want to insert a sequence that isn’t backed by a map.

Remember that map iteration order is unspecified. maps.All carries that property with it. If your result depends on which of several duplicate-producing inputs runs first, don’t rely on the order of a source map.

A nil destination map still panics

A subtle trap is assuming maps.Insert will allocate the destination. It won’t.

var dst map[string]int
maps.Insert(dst, maps.All(map[string]int{"a": 1}))

When the sequence yields "a", insertion attempts an ordinary assignment into a nil map, which panics. Allocate the destination first:

dst := make(map[string]int)
maps.Insert(dst, maps.All(map[string]int{"a": 1}))

There is one nuance: if the sequence yields no pairs, there is no assignment to perform. Passing a nil map with an empty sequence therefore doesn’t require allocation. Depending on that detail usually makes code harder to reason about, though. If a destination may receive data, initialize it before calling maps.Insert.

If what you actually want is a new map built from a sequence, use maps.Collect instead. Collect owns allocation; Insert owns mutation of an existing destination.

Stop producing work when the consumer stops

A custom iter.Seq2 receives a yield function whose boolean result tells the iterator whether iteration should continue. Well-behaved iterators return when yield returns false.

maps.Insert consumes the entire sequence, so under normal use its consumer doesn’t stop early. Still, writing iterator producers correctly matters because the same producer may later be passed to a consumer that does stop.

func pairs() iter.Seq2[string, int] {
	return func(yield func(string, int) bool) {
		for i, key := range []string{"a", "b", "c"} {
			if !yield(key, i) {
				return
			}
		}
	}
}

Keeping that contract intact makes the iterator reusable instead of accidentally coupling it to maps.Insert.

Don’t hide conflict rules inside a merge

Overwrite behavior is convenient until the application needs to distinguish “new key” from “conflicting key.” Consider configuration assembled from defaults and user input. If unknown keys should be rejected or duplicate definitions should produce an error, blindly inserting pairs loses the point where that validation should happen.

In those cases, write the rule explicitly:

for key, value := range seq {
	if _, exists := dst[key]; exists {
		return fmt.Errorf("duplicate key %q", key)
	}
	dst[key] = value
}

maps.Insert is a good fit when overwrite-on-conflict is already the desired policy. A shorter function call isn’t worth making a business rule invisible.

The same caution applies to concurrent access. A regular Go map isn’t made safe for concurrent writes because the writes happen through maps.Insert. Synchronization remains the caller’s responsibility.

Choose Insert when mutation is the operation you mean

Use maps.Insert when you have key-value pairs as an iterator and want to fold them into an existing map with normal overwrite semantics. Use maps.Copy when the source is simply another map, and maps.Collect when the sequence should become a new map.

That distinction keeps the code readable: the helper describes the data movement you actually intend. When conflict handling, ordering, or validation carries application meaning, keep those decisions explicit rather than squeezing them into a generic insertion step.