Go

How to Clone a Map in Go Without Sharing the Underlying Values

Clone Go maps safely with maps.Clone, maps.Copy, or a manual deep copy that prevents slices, maps, and pointers from being shared.

By Niko Minadze5 min read

Editorial illustration for How to Clone a Map in Go Without Sharing the Underlying Values

The short answer: use maps.Clone when you need a new map and ordinary assignment is sufficient for its values. If the values contain slices, maps, or pointers that must not be shared, allocate and copy those values yourself.

Assigning a map to another variable is not a clone at all.

Why assigning a map does not copy it

Maps are reference types. An assignment such as b := a gives you another name for the same map data, so writes through either variable are visible through the other.

package main

import "fmt"

func main() {
	a := map[string]int{"jobs": 1}
	b := a

	b["jobs"] = 2

	fmt.Println(a["jobs"]) // 2
	fmt.Println(b["jobs"]) // 2
}

No entries were copied here. This is useful when sharing is intentional, but it is an easy source of bugs when a function is expected to modify a private copy.

A shallow copy with maps.Clone

For Go 1.21 and later, the standard-library maps package provides Clone. Its generic signature is:

func Clone[M ~map[K]V, K comparable, V any](m M) M

Clone creates a new map and sets its keys and values using ordinary assignment. With values such as integers, replacing an entry in the clone does not replace the corresponding entry in the original:

package main

import (
	"fmt"
	"maps"
)

func main() {
	original := map[string]int{
		"retries": 3,
	}
	cloned := maps.Clone(original)

	cloned["retries"] = 10

	fmt.Println(original["retries"]) // 3
	fmt.Println(cloned["retries"])   // 10
}

The two variables now refer to different maps. Adding, deleting, or replacing an entry in cloned therefore does not make the same structural change to original.

The trap: Clone does not copy referenced data

maps.Clone is explicitly a shallow clone. If a value is a slice, map, or pointer, ordinary assignment copies that reference-like value—not the data it leads to.

The standard documentation illustrates the issue with a map[string][]int:

package main

import (
	"fmt"
	"maps"
)

func main() {
	m3 := map[string][]int{
		"key": []int{1, 2, 3},
	}
	m4 := maps.Clone(m3)

	fmt.Println(m4["key"][0]) // 1

	m4["key"][0] = 100

	fmt.Println(m3["key"][0]) // 100
	fmt.Println(m4["key"][0]) // 100
}

m3 and m4 are separate maps, but their "key" entries contain slices backed by the same data. Replacing the entire entry in m4 would leave m3 alone; modifying an element through the shared slice does not.

The rule is straightforward: maps.Clone copies the map, not what its values point to.

Deep-copying a map whose values are slices

When referenced values must be independent, allocate a new map and copy each value according to its type. For slices, that means allocating a new slice and copying its elements:

package main

import "fmt"

func cloneStringIntSlices(src map[string][]int) map[string][]int {
	dst := make(map[string][]int, len(src))

	for key, values := range src {
		if values == nil {
			dst[key] = nil
			continue
		}

		valuesCopy := make([]int, len(values))
		for i, value := range values {
			valuesCopy[i] = value
		}
		dst[key] = valuesCopy
	}

	return dst
}

func main() {
	original := map[string][]int{
		"ports": []int{8080, 8081, 8082},
	}
	cloned := cloneStringIntSlices(original)

	cloned["ports"][0] = 9090

	fmt.Println(original["ports"]) // [8080 8081 8082]
	fmt.Println(cloned["ports"])   // [9090 8081 8082]
}

This copies both levels: the map and every slice stored in it. The explicit nil check also preserves nil slice values rather than turning them into allocated empty slices.

There is no universal deep-copy operation because the correct policy depends on the value type. A struct may contain slices or pointers several levels down. A pointer may represent deliberately shared state. Nested maps may require recursion. Each additional allocation also costs time and memory, so copy only the levels that must be independent.

Before the standard-library maps package was added in Go 1.21, allocating a map and ranging over the source was the standard-library copying pattern. It remains the appropriate pattern when you need value-specific deep-copy behavior.

Merging with maps.Copy

Use maps.Copy when the destination map already exists and you want to merge another map into it. Its signature is:

func Copy[M1 ~map[K]V, M2 ~map[K]V, K comparable, V any](dst M1, src M2)

Every pair from src is added to dst. If both maps contain the same key, the source value overwrites the destination value.

package main

import (
	"fmt"
	"maps"
)

func main() {
	dst := map[string]int{
		"workers": 2,
		"timeout": 30,
	}
	src := map[string]int{
		"workers": 8,
		"retries": 3,
	}

	maps.Copy(dst, src)

	fmt.Println(dst["workers"]) // 8: overwritten
	fmt.Println(dst["timeout"]) // 30: retained
	fmt.Println(dst["retries"]) // 3: added
}

Like Clone, Copy uses ordinary value assignment and is shallow. Copying slice-valued entries into dst does not duplicate their underlying elements.

Choosing the right operation

Use the operation that matches the ownership you need:

  • Use maps.Clone for a fresh map when its values can safely be assigned as-is.
  • Use maps.Copy to add or overwrite entries in an existing map.
  • Use a hand-written loop when slices, nested maps, pointers, or other referenced data must not be shared.
  • Use plain assignment only when both variables are intentionally meant to access the same map.

One final edge case: the maps package performs no special handling for non-reflexive keys—keys for which k != k—such as floating-point NaNs. If a map uses such keys, cloning and other package operations do not compensate for their unusual equality behavior.

Comments

No comments yet.

Leave a comment

Your comment appears after approval. Plain text only; links stay as text. Line breaks and indentation are kept.

Up to 5,000 characters. No account or email needed.

All articles

Type at least two characters.

Press <kbd>Esc</kbd> to closeOpen the search page