Slice internals: append and aliasing

Go · Practical · 12 min

Most confusing slice bugs come from not knowing when append reuses memory and when it copies.

A slice header is three words: a pointer into a backing array, a length and a capacity. Passing a slice copies the header, not the elements, so the callee can change elements the caller sees — but if it appends, the caller's header still has the old length.

append(s, x) does one of two things:

  • If len(s) < cap(s), it writes x into the existing array and returns a header with a longer length. Any other slice sharing that array sees the write.
  • Otherwise it allocates a new, larger array (roughly doubling for small slices, growing more gently for large ones), copies, and returns a header pointing to the new array. From then on, old and new slices are independent.

That's why this bug is so easy to write: take a := base[:2] from a slice of capacity 4, append(a, 99) — and you have silently overwritten base[2]. Defences:

  • Use a full slice expression base[:2:2] to cap the capacity, forcing the next append to copy.
  • Clone with slices.Clone before handing a slice to code that may append.
  • If a function appends, return the new slice (s = add(s, x)) as append itself does.

When you know the final size, preallocate with make([]T, 0, n) to avoid repeated grow-and-copy cycles.

A small sub-slice of a huge array keeps the whole array alive for the garbage collector. When you keep a few bytes of a large buffer, copy them out instead of slicing.

Example

package main

import "fmt"

func main() {
	base := make([]int, 4, 4) // [0 0 0 0]
	a := base[:2]             // len 2, cap 4
	a = append(a, 99)         // fits: writes into base[2]
	fmt.Println(base)         // [0 0 99 0]

	b := base[:2:2]   // len 2, cap 2
	b = append(b, 7)  // no room: copies to a new array
	fmt.Println(base) // [0 0 99 0] (unchanged)
	fmt.Println(b)    // [0 0 7]

	out := make([]int, 0, 3) // preallocate capacity
	for i := range 3 {
		out = append(out, i)
	}
	fmt.Println(len(out), cap(out)) // 3 3
}

Remember

  • append writes in place while there's capacity, otherwise reallocates
  • s[lo:hi:max] limits capacity so append must copy
  • Preallocate with make([]T, 0, n) when the size is known

Watch out for

  • Appending to a sub-slice and unknowingly overwriting the parent's elements.

Check yourself

  1. `s := make([]int, 3, 10); t := append(s, 1); u := append(s, 2)` — what is `t[3]`?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: go.dev/blog/slices