When append Copies
Fast recall
Quick revision
Core answer
append copies only when length has reached capacity. WhileInterview-ready guide
Interview answer
The single condition is capacity. append checks whether the slice's
length is below its capacity; if it is, there is already a spare slot in
the backing array, so append writes there and increments the length —
no allocation, no copy, and any other slice sharing that storage sees the
write.
When length equals capacity, there is no spare slot, so append must grow: it allocates a new, larger backing array, copies the existing elements over, appends the new element, and returns a slice header pointing at the new storage. The original array is left behind. Executed:
s := []int{1, 2, 3} // len 3, cap 3 — full
u := append(s, 77) // must grow: new array, cap 6
s[0] = 5 // writes to the OLD storage
fmt.Println(s, u) // [5 2 3] [1 2 3 77] — forkeds and u printed different values because after the growth they are
two windows over two different arrays. Contrast the in-place case, where
the append fit inside existing capacity and every view saw it.
Two consequences matter in real code. First, append "may allocate" is
the honest description — you cannot rely on either branch, so never
ignore its return value (a common bug is append(s, x) without
capturing the result). Second, pre-sizing with make([]T, 0, n) when the
final size is known means every append writes in place — one allocation
up front instead of the allocate-copy-copy-copy chain of growth.
When append Copies
Direct Answer
append copies only when length has reached capacity. While
Deep Dive & Architecture
Capacity is the quiet third field of the slice header, and append's
behavior is a pure function of the comparison len == cap. Walk both
branches with the actual numbers and the mechanism locks in.
Branch one: capacity available
s := make([]int, 2, 3) // two used, one spare
t := append(s, 99) // fits: writes index 2 of the same array
s[0] = 5 // visible in t — still shared
// s: [5 2] t: [5 2 99]
One backing array the whole time. The append changed t's length from 2
to 3 and wrote into slot 2; s still has length 2, so it shows [5 2],
but the storage is shared — which is exactly why the s[0] = 5 write
appears in both. Efficient, and a booby trap for anyone who believed
append returns independent data.
Branch two: at capacity, the fork
s := []int{1, 2, 3} // len 3, cap 3 — no spare slot
u := append(s, 77) // allocate (cap → 6), copy 3, add 77
s[0] = 5 // lands in the OLD array
// s: [5 2 3] u: [1 2 3 77]
The growth is why u stopped tracking s: after the fork there are two
arrays. The doubling (3 → 6) is the growth strategy for small slices —
amortized constant time over many appends, at the cost of transiently
over-allocating.
The practical reading
Whenever correctness (not just performance) depends on whether two slices
share storage, you are in fork territory: either cap the sharing
deliberately (sub-slice, then never append) or break it deliberately
(copy). And when a function receives a slice, appends, and returns the
result, the caller's slice may or may not reflect the new elements —
which is why the idiomatic signature is return append(s, ...) and the
idiomatic call is always s = append(s, x).
Code Example
s := []int{1, 2, 3} // len 3, cap 3 — full
u := append(s, 77) // must grow: new array, cap 6
s[0] = 5 // writes to the OLD storage
fmt.Println(s, u) // [5 2 3] [1 2 3 77] — forked
Common Mistakes & Pitfalls
- Ignoring append's return value because 'it modifies in place' — only sometimes true.
- Assuming append always copies, then being surprised by aliasing.
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.