Copying Slices Safely
Fast recall
Quick revision
Core answer
t := s is NOT a copy — it shares backing storage; writes throughInterview-ready guide
Interview answer
A safe copy means new backing storage, and Go gives you three ways to get
it. The built-in is copy(dst, src), which transfers elements into
existing storage:
s := []int{1, 2, 3}
u := make([]int, len(s))
copy(u, s) // elements moved into u's own array
u[0] = 50
fmt.Println(s, u) // [1 2 3] [50 2 3] — independentThe second is the append idiom, which allocates and copies in one expression:
v := append([]int(nil), s...)
v[0] = 60
fmt.Println(s, v) // [1 2 3] [60 2 3] — also independentappend(nil, s...) starts from a nil slice (no storage) and appends the
elements of s, forcing allocation. Both executed forms leave s
untouched — that is the test of a real copy.
Choose between them by what you know: make+copy when the destination
size is known and you want exact capacity; the append idiom when you want
one short line and accept append's capacity choice. The third spelling —
make then element-by-element assignment — is the same as the first,
useful when the copy needs a transformation anyway.
The boundary case that bites: a slice of pointers (or slices containing
reference types) copies the pointers, not the objects — the copy shares
the pointees. For element types that are themselves references, "safe
copy" of the data requires copying the pointed-to values too, which is a
deep copy and outside copy's job.
Copying Slices Safely
Direct Answer
t := s is NOT a copy — it shares backing storage; writes through
Deep Dive & Architecture
Safe copying is the deliberate severing of the sharing that aliasing creates — so the deep version of this question is: what exactly does each option allocate, and what stays shared afterward?
What copy() does and does not do
copy(dst, src) is a memory transfer: it copies min(len(dst), len(src))
elements from one backing array into another. It never allocates — which
is why the idiom pairs it with make. Its return value (the number of
elements copied) matters when the lengths differ: copy stops at the
shorter side, so copying into an undersized destination silently copies
less than the whole source.
before: s ──▶ [1 2 3] u (make) ──▶ [0 0 0]
copy: transfer
after: s ──▶ [1 2 3] u ──▶ [1 2 3] (two arrays)
u[0]=50: s ──▶ [1 2 3] u ──▶ [50 2 3]
Why the append idiom is safe, precisely
append([]int(nil), s...) begins with a nil slice — len 0, cap 0, no
backing array. Spreading s... presents len(s) elements that cannot fit
(no capacity), so append takes the allocate-copy branch every time: fresh
storage, guaranteed. It is the same mechanism, used on purpose.
The pointer-slice boundary
Trace ps := []*int{&a, &b} and pc := append([]*int(nil), ps...): the
copy holds new pointer values — different headers — but each points at
the same a and b. *pc[0] = 99 changes a for every view of it.
The copy is safe at the slice level, shared at the data level; whether
that is acceptable depends on whether the elements are values (ints,
structs — fully safe) or references (pointers, maps, inner slices —
shared). That boundary, stated out loud, is the difference between a
candidate who has used copy and one who understands it.
Code Example
s := []int{1, 2, 3}
u := make([]int, len(s))
copy(u, s) // exact-size transfer
u[0] = 50
fmt.Println(s, u) // [1 2 3] [50 2 3] — independent
v := append([]int(nil), s...) // allocate-and-copy idiom
v[0] = 60
fmt.Println(s, v) // [1 2 3] [60 2 3] — also independent
Common Mistakes & Pitfalls
- Believing t := s is a copy.
- Copying slices of pointers and expecting deep isolation.
Looking for a structured roadmap?
Every module on this page is also part of the full Java backend interview track.