Passing Arrays vs Slices
Fast recall
Quick revision
Core answer
Interview-ready guide
Interview answer
Mechanically, the question is about what gets copied at the call boundary.
An array parameter receives a complete copy of the elements — a
[3]int copies three integers, a large array copies its entire contents,
every call. A slice parameter receives a copy of the header only — three
words — no matter how many elements it views. For large data the slice is
therefore the only reasonable default.
The behavior difference is just as important as the cost, and one executed pair shows both:
func modifyArr(a [3]int) { a[0] = 99 }
func modifySlc(s []int) { s[0] = 99 }
x := [3]int{10, 20, 30}
modifyArr(x)
fmt.Println(x) // [10 20 30] — the copy inside took the 99
s := []int{10, 20, 30}
modifySlc(s)
fmt.Println(s) // [99 20 30] — shared storage, the caller sees itSo the decision has two legitimate answers, depending on intent. When the function should read and possibly modify the caller's data — the common case — pass a slice. When the caller's data must remain untouched and the size is modest, an array (or an explicit defensive copy) is the honest choice: the copy is not overhead, it is the guarantee. The anti-pattern is passing large arrays around by value by accident — the cost repeats on every call and signals that the data wanted to be a slice.
Passing Arrays vs Slices
Direct Answer
Passing an array copies all of its elements on every call; passing a
Deep Dive & Architecture
The call boundary is where Go's "everything is passed by value" rule meets the array/slice split, and the result is one rule with two very different costs.
What the copy actually is
For an array, the copy is a full element-wise transfer — the same
operation as y := x, triggered at every call. For a slice, the copy is
the header: one pointer, one length, one capacity. This is why slice
passing is O(1) regardless of element count while array passing is O(n)
per call.
The mutation asymmetry, precisely
modifySlc(s) works because the function's header copy points at the
caller's storage — s[0] = 99 lands in shared memory. modifyArr(x)
silently "fails" to mutate because the function's array copy is private.
Neither is wrong; they are two contracts. The bug pattern is expecting
one contract and writing the other — say, a normalization function that
mutates its copy while the caller assumes normalization happened. The
fixes, in order of preference: return the new value, take a slice, or
take a pointer when in-place mutation is genuinely intended.
Why the idiomatic answer is "slice"
Standard-library Go settles the default: nearly every data-carrying API
takes slices, because O(1) passing plus clear shared-view semantics
compose well. The deliberate exceptions prove the rule — fixed-size
values like [32]byte digests flow by value precisely because their size
is small and their isolation is the point. When you choose, name which
property you are buying: cheap shared access (slice) or copy-protected
handoff (array).
Code Example
func modifyArr(a [3]int) { a[0] = 99 }
func modifySlc(s []int) { s[0] = 99 }
x := [3]int{10, 20, 30}
modifyArr(x)
fmt.Println(x) // [10 20 30] — the copy inside took the 99
s := []int{10, 20, 30}
modifySlc(s)
fmt.Println(s) // [99 20 30] — shared storage
Common Mistakes & Pitfalls
- Passing big arrays by value on every call out of habit.
- Expecting an array parameter to mutate the caller's data.
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.