skip to content

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