skip to content

Shallow and Deep Copying

Assignment copies a struct field by field, so an inner slice or map is still shared with the original, and slices.Clone and maps.Clone are shallow too. Go ships no deep copy at all.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What does Go copy when you assign a struct that has slice and map fields?

level: juniorimportance: must knowfreq 78%

answer

  1. the copy is one level deep
  2. fields are copied, not what they point at
  3. a slice field is pointer, length, capacity
  4. writing through the copy is visible through the original

basics

~20 s

Assignment copies a struct field by field. Number, string, bool and array fields become independent, but a slice, map or pointer field copies only the reference, so both structs still read and write the same underlying data.

solid answer

~40 s

Struct assignment in Go is a shallow, field-by-field copy. Scalar fields, arrays and nested struct fields are duplicated, so writing `b.Name` leaves `a.Name` alone. A slice field copies only its three-word header (pointer, length, capacity), a map field copies only the pointer to the map, and a pointer field copies the address, so `b.Tags["env"] = "prod"` or `b.Retries[0] = 9` is immediately visible through `a`. The same is true of passing a struct to a function, returning one, or storing it in a slice: the copy is one level deep. If you need the copy to be independent, you have to rebuild every reference-shaped field yourself; nothing in the language does it for you.

code

go · 13 lines
go
type Config struct {
	Name    string
	Tags    map[string]string
	Retries []int
}

a := Config{Name: "a", Tags: map[string]string{"env": "dev"}, Retries: []int{1, 2}}
b := a

b.Name = "b"             // a.Name is still "a"
b.Tags["env"] = "prod"   // a.Tags["env"] is now "prod"
b.Retries[0] = 9         // a.Retries[0] is now 9
b.Retries = nil          // a.Retries is untouched

go deeper

for a junior

Be ready to say that assignment copies a struct field by field, and to name which field types keep sharing data: slices, maps, pointers, channels and function values.

for a middle

Explain the mechanics: a slice field is a pointer, length and capacity, a map field is a pointer to the map structure, and only those words are copied. Show why reassigning a field differs from writing through it.

for a senior

Show where this bites in real code - value receivers, loop variables, structs stored in slices - and how you design a type so that copying it is either meaningful or impossible.

for a principal

Own the API-level consequence: whether your exported struct types are safe to copy at all, and whether callers can tell. A type whose copy silently aliases state needs either a documented ownership rule or a pointer-only design.

## The rule In Go, `b := a` on a struct value performs a **shallow copy**: the compiler copies the struct's memory, field by field, into a new location. There is no copy constructor, no `clone` hook, and no deep-copy machinery in the language. What that copy means depends entirely on what each field *is*. ## Fields that become independent A field whose value lives entirely inside the struct is genuinely duplicated: - numeric types, `bool`, `rune`, `byte` - `string` (the header is copied; the bytes behind it are shared, but strings are immutable so nothing can observe the sharing) - arrays such as `[3]int` - an array is a value, so all three elements are copied - nested struct fields, recursively, under these same rules - `time.Time`, `sync/atomic` scalars and other struct types that hold no reference fields Writing through `b` on any of these leaves `a` untouched. ## Fields that stay shared Three field shapes are *descriptors* that point at data stored elsewhere, so copying the descriptor does not copy the data: - **a slice field** - a slice value is a three-word header: a pointer to a backing array, a length, and a capacity. `b := a` copies those three words. Both `a.Retries` and `b.Retries` now address the same backing array, so `b.Retries[0] = 9` is visible as `a.Retries[0]`. - **a map field** - a map value is essentially a pointer to the map's internal structure. Both structs refer to the same map, so `b.Tags["env"] = "prod"` is a write both can see, and `delete(b.Tags, k)` removes the key for both. - **a pointer field** (`*User`, `*bytes.Buffer`, and so on) - the address is copied, so both structs point at the same object. Assigning `b.Owner = &other` rebinds only `b`, but `b.Owner.Name = "x"` mutates the shared object. Channels and function values behave like pointers here too: the copy refers to the same channel or the same closure. ## The asymmetry that trips people up The surprising part is not that references are shared - it is that *replacing* a field and *mutating through* a field behave differently. `b.Tags = nil` and `b.Retries = append(b.Retries, 4)` change only `b`'s own header or map reference. `b.Tags[k] = v` and `b.Retries[0] = 9` reach through to shared storage. So the same field can look isolated in one test and shared in the next, depending on which of the two operations the code performs. ## Where the copy happens Assignment is only one of the places Go copies a struct. All of these produce the same one-level copy: - passing a struct as a function argument, or returning one - a method call on a value receiver, where the receiver is a copy - appending a struct to a `[]T`, or storing it as a map value - ranging over `[]T` - the loop variable is a copy of each element - putting a struct value into an interface Because the copy is shallow every time, "I passed it by value, so the callee cannot affect my data" is only true for the scalar part of the struct. ## What to do about it If a copy really must be independent, you rebuild the reference fields explicitly: allocate a new map and re-insert the entries, allocate a new slice and copy the elements, allocate a new object for each pointer field. That work is yours to write - it is the whole content of a hand-written `Clone` method. If you do not want copies at all, keep the type behind a pointer (`*Config`) so nothing is silently duplicated, or design the struct so its fields are only scalars and arrays, which makes copying meaningful. ## Worked example ```go type Config struct { Name string Tags map[string]string Retries []int } a := Config{Name: "a", Tags: map[string]string{"env": "dev"}, Retries: []int{1, 2}} b := a b.Name = "b" b.Tags["env"] = "prod" b.Retries[0] = 9 ``` After this, `a.Name` is still `"a"`, but `a.Tags["env"]` is `"prod"` and `a.Retries[0]` is `9`. That one snippet contains the entire rule: the string was copied, the map and the slice were only referenced.

  • Does passing that struct to a function by value protect the caller's data?
    Only the scalar part of it. The parameter is the same one-level copy: the callee can reassign fields without the caller noticing, but `cfg.Tags[k] = v` or `cfg.Retries[0] = 9` writes into the caller's map and backing array. Passing by value is not a defence against mutation of reference-shaped fields.
  • Why does the copied `string` field not behave like the copied slice field?
    A string header also points at shared bytes, but Go strings are immutable: there is no operation that writes through a string, so the sharing is unobservable. A slice offers indexed assignment, which is exactly the operation that makes the sharing visible.
  • If a struct field is an array `[3]int` rather than a slice, what changes?
    An array is a value type, so all three elements are copied into the new struct. `b.Data[0] = 9` leaves `a.Data[0]` alone. That is the practical difference between `[3]int` and `[]int` as a field type when structs get copied around.

Copying the struct is like photocopying a form: the text typed on it is duplicated, but a phone number written on the form still rings the same phone.

saying these in an interview costs you the question

  • Says struct assignment produces a fully independent deep copy
  • Thinks a map field is duplicated because the struct is a value
  • Believes only pointer fields are shared, not slices and maps
  • Assumes passing a struct by value protects the caller's slice contents
  • Cannot distinguish reassigning a field from writing through it
open as a page

Why is copying a Go struct that contains a sync.Mutex a bug?

level: middleimportance: should knowfreq 46%

basics

~20 s

The mutex is copied as plain data, lock state included, so the copy is a second independent lock. Goroutines holding different copies enter the same critical section together. go vet's copylocks check reports it at build time.

open as a page

How do you deep copy a Go struct that holds a map of slices?

level: middleimportance: should knowfreq 58%

basics

~20 s

Go 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.

open as a page

A caller mutates Config.Headers after passing the Config by value into your library's New - why does that still change your client's behaviour, and what do you change?

level: seniorimportance: nice to knowfreq 34%

basics

~20 s

Passing the Config by value copies only the map's reference, so the caller and the library share one map. Clone the reference-shaped fields inside New, document which fields the library takes ownership of, and keep deliberately shared dependencies uncloned.

open as a page