
Because append does not mutate the slice header you passed in. It returns a new one. The extra elements live at the new length. If you throw that return away, or a helper appends and never returns the result, the original variable still has the old len. The compiler rejects a bare append call whose result is unused. A helper that appends to its own copy compiles fine.
The failure
Calling append and ignoring the result does not even compile:
s := []int{1, 2, 3}
append(s, 4) // error: append(s, 4) (value of type []int) is not used
So the compiler saves you there. The version that does compile hides the call in a helper.
The bug hides in a function. Element writes through the copied header can still hit the shared array. Length does not travel back unless you return it:
func add(s []int, x int) {
s = append(s, x)
}
func main() {
s := []int{1, 2, 3}
add(s, 4)
fmt.Println(s) // still [1 2 3]
}
Inside add, s is a copy of the header. append may even write 4 into spare capacity of the same array. The caller's len is still 3, so 4 is invisible. If append had to allocate a new array, the write is on a buffer the caller does not hold at all.
Why this happens
A slice value is {ptr, len, cap}. append looks at whether len + n <= cap.
- Room left: it writes into the existing array and returns a header with a bigger
len. The old header is unchanged. - No room: it allocates a new array, copies, and returns a header that points there. The old header still points at the old array.
Either way, the variable you care about only updates if you assign the return:
s = append(s, x)
That is not optional sugar. It is how the new len (and maybe the new ptr) gets stored.
This is the opposite of s[i] = v. Index assignment mutates the array. append mutates storage and then hands you a new view. Treat the old header as stale after a growth that reallocates. Treat it as "same array, shorter window" if you forgot the assignment.
The fix
Always capture the result. If a function grows a slice, return it (or take *[]T).
s := []int{1, 2, 3}
s = append(s, 4)
fmt.Println(s) // [1 2 3 4]
func add(s []int, x int) []int {
return append(s, x)
}
func main() {
s := []int{1, 2, 3}
s = add(s, 4)
fmt.Println(s) // [1 2 3 4]
}
append on nil is fine: var s []int; s = append(s, 1) allocates.
If you pass a slice into a helper and the helper must not steal spare capacity from the caller, cap it first (s[:len(s):len(s)]) or copy. That is the sibling bug: append did write the original array, just not through the header you are looking at.