skip to content

When does append write into a Go slice's existing backing array instead of a new one?

level: middleimportance: must knowfreq 78%

answer

  1. one number decides it
  2. length plus new elements versus capacity
  3. re-slicing never shrinks capacity
  4. the result may be a different array
  5. a[:2] still reaches a[2]

basics

~20 s

append writes in place when the new length still fits the slice's capacity. Otherwise it allocates a fresh array and copies into it. You cannot tell which happened at the call site, so always use append's return value.

solid answer

~40 s

`append(s, x)` checks whether `len(s)+1 <= cap(s)`. If it fits, it writes into the slice's existing backing array at index `len(s)` and returns a slice with length one greater — so any other slice over that same array sees the write. If it does not fit, append allocates a new array, copies the elements across and returns a slice pointing at the new array, leaving the original untouched. That is why `b := append(a[:2], 99)` can silently overwrite `a[2]`: `a[:2]` inherits `a`'s full capacity. Two consequences follow: you must always assign the result, since the returned slice may describe a different array; and you should not hand out a subslice you or the caller will append to without copying it or capping its capacity.

code

go · 6 lines
go
a := []int{1, 2, 3, 4}
fmt.Println(len(a[:2]), cap(a[:2])) // prints: 2 4

b := append(a[:2], 99) // room to spare: writes a[2]
fmt.Println(a)         // prints: [1 2 99 4]
fmt.Println(b)         // prints: [1 2 99]

go deeper

for a junior

Be ready to say that you must write s = append(s, x) and use what append returns, because the returned slice may describe a different array than the one you passed in.

for a middle

Explain the deciding condition in terms of length and capacity, and walk through the a[:2] example showing exactly which index gets written and why the original slice sees it.

for a senior

Demonstrate the review instinct: flag any API that hands out a subslice of a buffer it keeps appending to, and say what you would require instead — a copy at the boundary, or a capacity-capped result.

for a principal

Own the codebase rule for shared buffers: where copies are mandatory at package boundaries, and how you keep that cost visible rather than letting every caller defensively clone every slice.

## What append actually does `append` is a builtin, not a method, and its contract is small: given a slice and some elements, it returns a slice containing the original elements followed by the new ones. Nothing in that contract promises where the result lives, and that is the whole subject. A slice is described by three things: a pointer to a backing array, a length (how many elements this slice exposes) and a capacity (how many elements exist from the pointer to the end of that array). `append(s, x)` asks one question: does `len(s) + 1` fit in `cap(s)`? - **It fits.** append writes `x` into the backing array at index `len(s)` — an element that already existed in the array but was outside `s`'s view — and returns a slice with the same pointer and a length one larger. **No allocation, no copy, and the write is visible through every other slice that covers that array position.** - **It does not fit.** append allocates a new, larger array, copies the existing elements into it, writes `x`, and returns a slice pointing at the new array. **The original array is untouched, and the original slice keeps seeing the old contents.** Both outcomes are correct behaviour. You cannot tell from the call site which one happened, and you must not write code whose correctness depends on knowing. ## The aliasing surprise The classic demonstration: ```go a := []int{1, 2, 3, 4} b := append(a[:2], 99) ``` `a[:2]` has length 2 but capacity 4 — re-slicing never shrinks capacity, it only moves the length. So append has room, writes 99 into the shared array at index 2, and `a` becomes `[1 2 99 4]` while `b` is `[1 2 99]`. The programmer who wrote it was thinking "take the first two and add one"; what they got was a mutation of a slice they never named on that line. The same thing arrives less obviously through a function boundary. If you pass a slice to something that appends to it, the callee may write into your array without you ever seeing the returned slice. Passing a slice is not a defensive copy: the elements are shared, and only the length is private to each copy of the descriptor. ## Why you must assign the result Because the returned slice may point at a different array, and always has a different length, the value append gives back is the only correct slice to keep using. `append(s, x)` on its own is in fact a compile error in Go — the result of append may not be discarded — but a subtler version of the same mistake survives: appending inside a function and expecting the caller's variable to grow. ```go func addOne(s []int) { s = append(s, 1) } // caller sees nothing ``` The callee's `s` is its own descriptor. Growing it changes the callee's length only. To let the caller see the new element, return the slice (`func addOne(s []int) []int`) or take a `*[]int`. ## How to write code that does not depend on the outcome 1. **Always use the return value**, and assign it back to the variable you intend to keep: `s = append(s, x)`. 2. **Never assume append copied.** If you need an independent slice, make one deliberately: allocate with `make` and `copy` into it. `copy(dst, src)` moves `min(len(dst), len(src))` elements and returns that count, so the destination's length — not its capacity — is what decides how much lands. 3. **Never assume append wrote in place** either. Code that appends to one slice and then reads the change through another is broken as soon as the capacity runs out on a different input. 4. **Do not hand out subslices you will keep appending to.** If a method returns `s[:n]` and the receiver later appends, the caller's data can be overwritten. Either copy before returning, or cap the returned slice's capacity with a full slice expression so the caller's append is forced to allocate. ## The interview signal A weak answer says "append adds an element to the slice". A strong one names capacity as the deciding condition, states that the result may alias or may not, and draws the practical rule from it: the returned slice is the truth, and sharing a backing array between two slices that both append is a bug waiting for the right input length.

  • Why does `b := append(a[:2], 99)` change a, when a[:2] looks like a shorter slice?
    Re-slicing moves the length but keeps the capacity: `a[:2]` has length 2 and capacity 4, still pointing at `a`'s array. append sees room at index 2, writes 99 into the shared array, and returns a length-3 slice. `a` reads that same array, so it now shows `[1 2 99 4]`.
  • A function takes a []int and appends to it. Why might the caller not see the new element?
    The slice descriptor is passed by value, so the callee's length is its own. Growing it inside the function leaves the caller's length unchanged — and if append reallocated, the callee is writing into a different array entirely. Return the slice, or accept a `*[]int`, if the caller must observe the growth.
  • How do you guarantee that a slice you keep is independent of the one you were given?
    Allocate and copy explicitly: `dst := make([]int, len(src)); copy(dst, src)`. copy transfers `min(len(dst), len(src))` elements and returns that number, so a destination with length 0 copies nothing regardless of its capacity. Relying on append to have reallocated is not a guarantee — it depends on the input's capacity.
  • Does append ever reorder or move elements the slice already held?
    It never reorders them. If it reallocates, existing elements are copied to the new array in the same order and the new ones follow; if it writes in place, existing elements are not touched at all. What changes is only which array the returned slice points at, and its length.

Appending is like adding a book to a shelf: if the shelf has a free slot you push it in and everyone sharing that shelf sees the new book. If the shelf is full you buy a bigger shelf, move the books across, and the people still looking at the old shelf see nothing new.

saying these in an interview costs you the question

  • Says append always allocates a new array
  • Says append always mutates in place
  • Believes passing a slice to a function copies its elements
  • Thinks a[:2] has capacity 2
  • Expects a callee's append to grow the caller's slice