Why does returning a Go struct by value still share its []float64 field between callers?
answer
- struct assignment copies field by field
- one of the fields is only three words
- both copies hold the same array address
- an array field would have been copied whole
basics
~20 sCopying a struct copies a slice field's three-word header, not the elements. Both copies hold the same backing-array pointer, so a write through one is seen by the other. An array field like [16]float64 is copied element by element instead.
solid answer
~40 sStruct assignment in Go is field by field and shallow. A `[]float64` field is three words - pointer, length, capacity - so the copy duplicates those words and nothing else; both structs now name the same backing array, and a write to `w.Recent[0]` through either one is visible through the other. Returning by value isolates the *fields*, never what a pointer-bearing field points at. If a library must hand out independent data it either uses a fixed-size array field such as `[16]float64`, which really is copied element by element, or makes a defensive copy with `slices.Clone` at the package boundary. In a debugger the giveaway is immediate: the two supposedly independent structs show the same data pointer in their slice headers.
code
go · 9 linestype Window struct {
Fixed [4]float64
Recent []float64
}
func (w Window) Scribble() {
w.Fixed[0] = 99 // only this copy's array
w.Recent[0] = 99 // the caller's backing array
}go deeper
Know that assigning or returning a struct copies its fields one at a time, and that copying a slice field duplicates only its header while the elements stay where they are.
Explain why: the slice field is three words, so the copy duplicates pointer, length and capacity and never the backing array those words describe.
Be ready to diagnose it - compare the data pointers of the two headers in a debugger - and to name the fixes: a fixed-size array field, or a defensive copy at the package boundary.
Own the boundary decision. Whether an exported struct hands callers a snapshot or a shared window is a contract you cannot quietly change later, and it trades an allocation per call against isolation.
## What "copy the struct" actually copies Assignment, argument passing, returning, and a value receiver all copy a struct **field by field**, using each field's own copy semantics. Nothing about a struct copy is deep. Whether a copy is genuinely independent therefore depends entirely on what its fields are. Consider a measurement window a library hands to its callers: ``` type Window struct { Fixed [4]float64 Recent []float64 } ``` `Fixed` is an array. Its length is part of its type, and the value *is* its four float64s - 32 bytes. Copying the struct copies all four elements, so the copy is independent. `Recent` is a slice. Its value is a three-word header: a pointer to the first element in a backing array, a length, and a capacity. Copying the struct copies those three words. It does not, and cannot, copy the array behind the pointer - the compiler does not know how big it is at compile time and would not copy it if it did. Both structs now hold the same pointer. The result is a value that looks independent and behaves like a shared view for exactly one of its fields. ## The failure it produces The bug this causes is a copy that is not a copy. A caller receives a `Window` by value, edits `Recent[0]`, and a completely different caller - or the library's own internal state - sees the edit. Nothing in the type signature warns anyone. `Window` is returned by value, which every reader takes as a promise of isolation, and for `Fixed` it is one. The same trap applies to any pointer-bearing field: a `*T`, a map, a channel, or a struct that itself contains one of those. Slices are the case that surprises people most, because a slice looks like a container rather than a reference. ## Diagnosing it Two things settle it quickly. A debugger is the most direct. Stop on the return, expand both structs, and compare the slice fields: the length and capacity words may match innocently, but if the **data pointers** are equal, the two structs share elements and the "copy" is a fiction. This is one of the few bugs where a single glance at the raw header words is conclusive. Failing that, the behavioural test is trivial: take the copy, write a sentinel into the slice field, and read the original. If the sentinel is there, the field is shared. ## The library author's options **Use a fixed-size array field.** If the data really is a fixed number of measurements, `[16]float64` gives true value semantics: independent on copy, and comparable, so a struct built only from comparable fields can be tested with `==` and used as a map key. A struct containing a slice is not comparable at all - `==` on it is a compile error - which is a second, often-forgotten consequence of the same design. The cost is that every copy moves 128 bytes rather than 24, so this suits small, genuinely fixed shapes and not large ones. **Copy defensively at the boundary.** Return `slices.Clone(w.Recent)` (or clone the field before returning the struct) so each caller gets its own array. This costs an allocation per call, which is the honest price of isolation, and it is usually the right call for an exported API whose callers you do not control. **Copy on the way in as well.** A constructor that stores a caller-supplied slice is the mirror image of the same bug: the caller keeps a header into your internal array and can mutate your state afterwards. If the struct is meant to own its data, clone at the door. **Or document the sharing and mean it.** Sometimes sharing is the point - a view over a large buffer that you deliberately do not want to duplicate. That is a legitimate design, but it must be stated in the doc comment, because the type alone says the opposite. ## Why this decision is expensive to revisit An exported struct's field types are part of your API. Changing `Recent []float64` to `Fixed [16]float64`, or starting to clone on every return, changes both the compile-time shape and the observable aliasing behaviour for every team that imports the package. Callers may already, knowingly or not, depend on the sharing. That is why the question belongs at design time: decide whether the value you hand out is a snapshot or a window, encode it in the field type where you can, and say so in the documentation where you cannot.
- For a fixed window of sixteen measurements in an exported struct, would you declare [16]float64 or []float64?`[16]float64` when the count really is fixed. It gives each copy independent elements, keeps the struct comparable so it can be a map key, and removes an allocation. The cost is 128 bytes copied on every pass. `[]float64` is right when the length varies, but then the package owes callers either a defensive copy or a documented statement that the data is shared.
- How would you prove in a debugger that two copies of the struct share elements?Stop after the copy and expand both structs' slice fields. Each shows three words; equal length and capacity prove nothing, but equal data pointers prove the two headers describe the same backing array. Writing a sentinel through one and reading it through the other is the behavioural confirmation.
- Does the same problem appear when a constructor stores a caller-supplied slice?Yes, in mirror image. Storing the caller's slice keeps their header alive alongside yours over one array, so they can mutate your internal state after construction. If the struct is meant to own its data, clone the slice on the way in as well as on the way out.
Photocopying a form copies the address written on it, not the house at that address. Two people holding photocopies can both redecorate the same living room.
saying these in an interview costs you the question
- Says Go copies structs deeply, including slice contents
- Assumes returning by value always isolates state
- Believes only explicit pointer fields are shared
- Treats [16]float64 and []float64 as interchangeable fields
- Thinks a struct with a slice field can be compared with ==