Slice Aliasing
Fast recall
Quick revision
Core answer
t := s or t := s[:2] copies only the header — both slices point atInterview-ready guide
Interview answer
It happens because slice assignment copies a three-field header, not the
elements. After t := s, there are two headers and one backing array;
both slices are views over the same memory, so a write through either
view is a write to the shared block.
The surprising case is append, because it does not always allocate:
s := []int{1, 2, 3} // len 3, cap 3
t := s[:2] // len 2, cap 3 — capacity available
t = append(t, 42)
fmt.Println(s, t) // [1 2 42] [1 2 42]At the moment of the append, t has length 2 but capacity 3 — one spare
slot inside the existing backing array. append sees capacity, so it
writes 42 directly into that slot. The slot it wrote into is s's third
element, so s now prints [1 2 42] even though nobody touched s
directly. This is the mechanism behind the most common Go
data-corruption report: "I appended to my sub-slice and my original slice
changed."
The sharing continues until a growth forces a fork: once a slice is at
capacity and appends again, append allocates new storage, copies, and
returns a header pointing elsewhere — the two slices finally diverge. The
safe patterns when independence is required are copy(dst, src) or
append([]int(nil), s...), both of which produce genuinely separate
backing storage. The rule to carry away: assignment and re-slicing share
storage below capacity, and only growth severs it.
Slice Aliasing
Direct Answer
t := s or t := s[:2] copies only the header — both slices point at
Deep Dive & Architecture
The header picture makes aliasing predictable instead of spooky — and predictability is the point, because the follow-up questions (when does append copy? how to copy safely?) are all this one mechanism.
Two windows, one row
Start from the state the snippet creates:
s: ptr ──▶ [ 1 2 3 ] ◀── ptr: t
len 3, cap 3 len 2, cap 3
t := s[:2] reads as "make another window on the same storage, showing
two elements, with capacity to see three." No element was copied at any
point — only a small header.
Watching the write land
Trace the append precisely: t has length 2, capacity 3, so slot index 2
of the backing array is beyond t's length but within its capacity —
allocated, uninitialized, and reachable. append(t, 42) increments the
length to 3 and stores 42 at index 2. Because the row is shared, s —
whose length was already 3 — now observes 42 at its third element. The
printed pair [1 2 42] [1 2 42] is the executed proof.
When sharing is a feature
Aliasing is not a defect to avoid — it is how sub-slicing is efficient.
Every s[i:j] in hot code paths shares storage precisely so that no
copy happens. The discipline is knowing which operations preserve sharing
(s[i:j], in-capacity append, copy between overlapping windows) and
which sever it (out-of-capacity append, explicit copy to fresh
storage). When you hand data to code that may modify it and the original
must stay pristine, that is the moment to reach for a real copy.
Code Example
s := []int{1, 2, 3} // len 3, cap 3
t := s[:2] // len 2, cap 3 — capacity available
t = append(t, 42)
fmt.Println(s, t) // [1 2 42] [1 2 42]
Common Mistakes & Pitfalls
- Believing append always allocates fresh storage.
- Treating aliasing as a Go bug rather than header semantics.
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.