Back to blog

Sunday, August 16, 2026

Why Did append() Not Update My Original Slice?

Title card for 'Why Did append() Not Update My Original Slice?' with a short code example

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.