Copying a Go map is easy to get subtly wrong. Assigning one map variable to another doesn’t duplicate the map, so a write through either variable changes the same underlying map. Since Go 1.21, the standard library’s maps.Clone function gives you a concise way to make a separate top-level map.

There is one boundary worth understanding before using it: maps.Clone is a shallow clone. Adding, deleting, or replacing entries in the clone won’t change the original map, but nested maps, slices, pointers, and other reference-bearing values can still refer to the same underlying data.

That distinction determines whether maps.Clone is exactly the tool you need or only the first step toward a true independent copy.

Why map assignment doesn’t copy a map

Consider a small configuration map:

package main

import "fmt"

func main() {
    original := map[string]int{
        "http":  8080,
        "admin": 9090,
    }

    copied := original
    copied["http"] = 8081
    delete(copied, "admin")

    fmt.Println(original)
}

The output reflects both changes:

map[http:8081]

copied := original copies the map value, not all of its entries into independent storage. Both variables still refer to the same map state.

That behavior is useful when sharing a map is intentional, but it can be surprising around configuration snapshots, default values, request-specific overrides, or tests that expect to modify a temporary copy.

Before Go 1.21, the usual explicit copy looked like this:

copied := make(map[string]int, len(original))
for key, value := range original {
    copied[key] = value
}

That loop is still valid. maps.Clone packages the same top-level copying intent into a standard generic function.

Copy a Go map with maps.Clone

For a basic map, cloning is one call:

package main

import (
    "fmt"
    "maps"
)

func main() {
    original := map[string]int{
        "http":  8080,
        "admin": 9090,
    }

    copied := maps.Clone(original)
    copied["http"] = 8081
    delete(copied, "admin")
    copied["metrics"] = 9100

    fmt.Println("original:", original)
    fmt.Println("copied:  ", copied)
}

The maps now have independent top-level state. Mutating entries in copied doesn’t add, remove, or replace entries in original.

Conceptually, the operation is close to:

original
  "http"  -> 8080
  "admin" -> 9090

          maps.Clone
              |
              v

copied
  "http"  -> 8080
  "admin" -> 9090

The entries are assigned into another map. This is the key detail behind both the usefulness and the limitation of maps.Clone.

The function also preserves a nil map:

var original map[string]int
copied := maps.Clone(original)

fmt.Println(copied == nil) // true

That differs from manually allocating an empty map unconditionally. If your code distinguishes nil from non-nil empty maps, maps.Clone preserves that distinction.

maps.Clone makes a shallow copy

The standard library describes maps.Clone as a shallow clone: keys and values are copied using ordinary assignment. For scalar values such as integers, booleans, and strings, that’s often all you need.

The situation changes when a map value contains another reference-bearing value.

Suppose each service has labels:

package main

import (
    "fmt"
    "maps"
)

type Service struct {
    Labels map[string]string
}

func main() {
    original := map[string]Service{
        "api": {
            Labels: map[string]string{
                "tier": "backend",
            },
        },
    }

    copied := maps.Clone(original)

    service := copied["api"]
    service.Labels["tier"] = "changed"
    copied["api"] = service

    fmt.Println(original["api"].Labels["tier"])
}

This prints:

changed

The outer maps are separate, but the Labels field in each copied Service still refers to the same nested map.

The same caveat applies to slices and pointers. A copied slice header can still refer to the same backing array, and a copied pointer still points to the same object. Interfaces can also contain values with reference semantics, so map[string]any deserves particular care if callers expect a deep copy.

A useful way to think about the boundary is:

original outer map          cloned outer map
       |                            |
       +---- value copy ------------+
                 |
                 v
          shared nested data

maps.Clone separates the container. It doesn’t recursively walk the object graph behind its values.

When a shallow map clone is enough

A shallow clone works well when the map’s keys and values don’t contain mutable shared state.

For example:

type Route struct {
    Port    int
    Enabled bool
}

routes := map[string]Route{
    "api": {Port: 8080, Enabled: true},
}

copyOfRoutes := maps.Clone(routes)
copyOfRoutes["api"] = Route{Port: 8081, Enabled: true}

Route contains only value-like fields. Replacing an entry in the cloned map cannot mutate the struct stored in the original map.

Other common fits include maps of strings, numbers, booleans, enums, small structs containing only value-like fields, and lookup tables that are copied before adding or removing entries.

There’s no need to build a recursive copier merely because the word “clone” sounds incomplete. If the values are immutable by convention, or all you need is independent membership and entry replacement, a shallow clone is simpler and easier to audit.

Deep-copy nested values deliberately

When nested mutable data must be independent, clone it explicitly according to its type. That is usually clearer than trying to invent a generic deep-copy helper.

For a map whose values contain another map:

package config

import "maps"

type Service struct {
    Labels map[string]string
}

func CloneServices(src map[string]Service) map[string]Service {
    dst := maps.Clone(src)

    for name, service := range dst {
        service.Labels = maps.Clone(service.Labels)
        dst[name] = service
    }

    return dst
}

Now both levels are copied. A change such as:

cloned["api"].Labels["tier"] = "internal"

won’t alter the original labels map.

Slices need similar treatment. The standard library’s slices.Clone is a natural companion when a struct contains a slice:

import (
    "maps"
    "slices"
)

type Service struct {
    Labels map[string]string
    Ports  []int
}

func CloneServices(src map[string]Service) map[string]Service {
    dst := maps.Clone(src)

    for name, service := range dst {
        service.Labels = maps.Clone(service.Labels)
        service.Ports = slices.Clone(service.Ports)
        dst[name] = service
    }

    return dst
}

This kind of type-aware copying has an advantage: the code documents exactly which fields carry mutable state. If the type later gains a pointer, another map, or a nested slice, the cloning function is an obvious place to review.

Deep copying gets harder when data contains cycles, shared subgraphs that should remain shared, mutexes, file handles, channels, or objects whose identity matters. At that point, “copy everything recursively” isn’t a neutral operation. The application needs to define what an independent copy actually means.

Don’t use cloning as a concurrency primitive

A separate map can be useful for copy-on-write designs, but maps.Clone by itself doesn’t make concurrent map access safe.

This is unsafe if another goroutine can write current while it is being cloned:

snapshot := maps.Clone(current)

Cloning has to read the source map. The ordinary rules for concurrent map access still apply, so synchronization must protect the source while a mutable map can be written.

A simple mutex-protected snapshot might look like this:

type Registry struct {
    mu     sync.RWMutex
    routes map[string]int
}

func (r *Registry) Snapshot() map[string]int {
    r.mu.RLock()
    defer r.mu.RUnlock()

    return maps.Clone(r.routes)
}

The lock protects the read during cloning. After the function returns, callers can change the returned top-level map without modifying r.routes.

That last guarantee still depends on the values. If routes were map[string]*Route, callers could mutate a shared Route through a pointer even though they couldn’t change the registry’s map membership. Decide which level of isolation your API promises, then copy and synchronize at that level.

Choose maps.Clone or maps.Copy based on the operation

The maps package also provides maps.Copy, but it solves a different problem.

maps.Clone(src) creates and returns a new map with the source entries. maps.Copy(dst, src) adds source entries to an existing destination and overwrites destination values when keys collide.

That makes maps.Copy convenient for overlays:

defaults := map[string]string{
    "region": "us-east",
    "mode":   "safe",
}

overrides := map[string]string{
    "region": "eu-west",
}

config := maps.Clone(defaults)
maps.Copy(config, overrides)

The resulting config starts from defaults and applies overrides, while the original defaults map remains unchanged.

A common mistake is to call maps.Copy with the original map as the destination and assume a copy was created. It wasn’t; that call intentionally mutates its destination. Clone first when you need a new top-level map.

Test the isolation you actually require

A good cloning test should mutate the copy after cloning. Equality immediately after the clone only proves that the initial contents match.

For a shallow-copy contract, test top-level isolation:

func TestCloneTopLevelMap(t *testing.T) {
    original := map[string]int{"api": 8080}
    cloned := maps.Clone(original)

    cloned["api"] = 9090
    cloned["admin"] = 9000

    if original["api"] != 8080 {
        t.Fatalf("original api port changed: %d", original["api"])
    }
    if _, exists := original["admin"]; exists {
        t.Fatal("clone mutation added a key to original")
    }
}

If your own helper promises a deeper copy, mutate every nested mutable field that the contract says should be independent. This catches the easy failure mode where an outer maps.Clone looks correct but a nested slice or map is still shared.

Also test nil behavior if it matters to your API. A nil map and an allocated empty map both have length zero, but they aren’t always interchangeable in serialization, API conventions, or code that explicitly checks for nil.

Copy only as deeply as your contract requires

maps.Clone is a good default when you need another Go map with independent top-level entries. It removes boilerplate and makes the intent obvious: this code wants a new map, not another reference to the same one.

The name shouldn’t be read as a promise of recursive independence. Inspect the value type. If it contains maps, slices, pointers, or other mutable references, decide whether sharing them is acceptable. When it isn’t, clone those fields deliberately and test by mutating the result.

That gives you a practical rule for reviews: use maps.Clone to separate map state, then copy deeper only where the data model says independence is part of the contract.