How do you deep copy a Go struct that holds a map of slices?
answer
- the language gives you no clone
- one helper undoes one level of sharing
- a new map is not new slices
- copy the struct first, then rebuild each reference field
- stop where sharing is intentional
basics
~20 sGo has no built-in deep copy. Write a Clone method: copy the struct, then allocate a new map, and for every entry clone the slice value before inserting it. Recurse until nothing mutable is shared.
solid answer
~40 sThere is nothing in Go that deep copies for you, so you write a `Clone` method by hand. Start with `c := p`, which copies the scalar fields, then rebuild every reference-shaped field: `make` a new map with `len(p.Rules)` capacity, range over the original, and for each entry replace its slice with `slices.Clone` before inserting the value into the new map. `slices.Clone` and `maps.Clone` are explicitly shallow - the elements are copied by assignment - so `maps.Clone` on a `map[string][]string` gives you a new map whose values still alias the original slices. That is exactly one level, which is why the nested loop is unavoidable. Pointer fields need a fresh allocation of the pointed-to value. Decide deliberately how deep to go: shared loggers or connections are usually meant to stay shared.
code
go · 23 linestype Rule struct {
Name string
Hosts []string
}
type Policy struct {
Rules map[string]Rule
Owner *User
}
func (p Policy) Clone() Policy {
c := p // scalars copied; Rules and Owner still shared
c.Rules = make(map[string]Rule, len(p.Rules))
for k, r := range p.Rules {
r.Hosts = slices.Clone(r.Hosts) // r is already a copy of the map value
c.Rules[k] = r
}
if p.Owner != nil {
owner := *p.Owner
c.Owner = &owner
}
return c
}go deeper
Know that Go has no built-in deep copy and that you must allocate a new map or slice and copy entries across yourself.
Be able to write the Clone method on the spot for a nested type, and explain precisely which level each standard helper fixes and which it leaves shared.
Show the judgment about depth: which fields are data to clone and which are collaborators to share, plus the allocation cost and the test that keeps the clone honest as the struct grows.
Decide whether the type should be clonable at all. Immutable values, copy-on-write, or handing out a read-only view often beat maintaining a deep-copy method that every new field can quietly break.
## There is no deep copy in the language Go gives you exactly one copying operation - assignment - and it is one level deep. Every deep copy in a Go program is code somebody wrote. The idiomatic shape is a method: ```go func (p Policy) Clone() Policy ``` or `func (p *Policy) Clone() *Policy` when the type is normally used through a pointer. Naming it `Clone` matches the standard library's own vocabulary (`slices.Clone`, `maps.Clone`, `net/url.URL.Clone`). ## Why the standard helpers do not finish the job `slices.Clone(s)` returns a new slice whose elements are copied **by assignment**, and `maps.Clone(m)` returns a new map whose keys and values are set by ordinary assignment. Both are documented as shallow. The `copy(dst, src)` builtin is the same thing at a lower level: it copies `min(len(dst), len(src))` elements, again by assignment. So for `map[string][]string`: - `maps.Clone` gives you a genuinely new map - inserting or deleting a key no longer affects the original - but every value in it is the *same slice header* as before, so `clone["a"][0] = "x"` writes into the original's backing array One level of cloning fixes one level of sharing. A map of slices has two levels, so you need two steps. ## The shape of a correct clone ```go type Rule struct { Name string Hosts []string } type Policy struct { Rules map[string]Rule Owner *User } func (p Policy) Clone() Policy { c := p // scalars copied; Rules and Owner still shared c.Rules = make(map[string]Rule, len(p.Rules)) for k, r := range p.Rules { r.Hosts = slices.Clone(r.Hosts) // r is already a copy of the value c.Rules[k] = r } if p.Owner != nil { owner := *p.Owner c.Owner = &owner } return c } ``` Three details in that code carry the whole technique: 1. `c := p` first, so you never forget a scalar field when the struct grows. 2. The loop variable `r` is already a copy of the map value, so mutating `r.Hosts` before inserting is safe - and necessary, because map elements are not addressable and cannot be modified in place. 3. The `nil` check before dereferencing `p.Owner`; a deep copy that panics on a nil pointer field is the most common bug in hand-written clones. ## Deciding how deep "deep" goes A deep copy is not automatically the right answer for every field. Ask, per field, whether the copy is meant to be an independent value or to keep talking to the same collaborator: - data the caller may mutate: clone it - a `*log.Logger`, a `*sql.DB`, a channel, a `context.Context`: share it deliberately, do not clone it - an immutable field such as a `string` or a `time.Time`: plain assignment is already correct - a field holding a lock: a struct containing a `sync.Mutex` must not be copied at all, so a value-receiver `Clone` on such a type is itself a bug Document the answer. A `Clone` whose depth is undocumented is a trap for the next reader, who will assume it is total. ## Failure modes to watch for - **Cycles.** A hand-written recursive clone over a graph that points back at itself will recurse forever. If your data can be cyclic you need a `map[*T]*T` of already-copied nodes, which is a strong hint that a deep copy is the wrong design. - **Silent drift.** A `Clone` that lists fields explicitly (`return Policy{Rules: ..., Owner: ...}`) goes stale the moment somebody adds a field. Starting from `c := p` and overriding the reference fields keeps new scalar fields correct by default; a test that clones, mutates the source, and asserts the clone is unchanged is what catches the rest. - **Cost.** A deep copy allocates: one allocation per map and per slice, at least. On a hot path that is often more expensive than the mutation you were defending against. Measure with a benchmark using `-benchmem` before making cloning the default. - **Reflection.** A generic reflect-based deep copier looks attractive and is a poor trade: it is slow, it cannot see unexported fields in other packages, and it copies things that were meant to be shared. Hand-written clones are boring and correct. ## The short version Copy the struct, then rebuild every field that is a slice, map, pointer or nested struct containing one of those - and stop wherever sharing is intentional.
- If maps.Clone gives you a new map, what is still shared afterwards?Every value in it. `maps.Clone` sets the new keys and values by ordinary assignment, so a `map[string][]string` clone holds the same slice headers, and a `map[string]*User` clone holds the same pointers. Adding or deleting keys is now independent; mutating a value is not.
- How would you test that a Clone method is actually deep?Clone the value, mutate every reference-shaped field of the original - write into each slice element, insert and delete map keys, mutate through each pointer - then assert the clone is unchanged. Add a case with nil fields, since a nil pointer or nil map is where hand-written clones usually panic.
- When would you deliberately not clone a field?When sharing is the point: a logger, a database handle, a channel, a `context.Context`, or any collaborator the copy is supposed to keep using. Cloning those breaks behaviour rather than protecting it. The rule is to clone data the caller may mutate, and share dependencies.
- What goes wrong with a reflection-based generic deep copier?It is slow relative to hand-written code, it cannot read unexported fields of types in other packages, it duplicates fields that were meant to be shared, and it loops forever on cyclic data unless you track visited pointers. A short explicit Clone per type is simpler and faster.
saying these in an interview costs you the question
- Thinks maps.Clone or slices.Clone produces a deep copy
- Says the copy builtin duplicates the elements' own contents
- Writes a Clone that lists fields explicitly and drifts as fields are added
- Dereferences a pointer field without a nil check
- Deep copies loggers, handles and channels that were meant to be shared
- Reaches for a reflection-based deep copier as the default