Why must you write s = append(s, x) in Go instead of just calling append(s, x)?
answer
- the slice value is copied, not shared
- three words: pointer, length, capacity
- append may hand back a different array
- a length change lives only in the return value
- the compiler rejects a discarded append
basics
~20 sappend returns a new slice value and never updates the slice you passed in. If the backing array has no spare room, append allocates a bigger array, so only the returned value is guaranteed to see the appended element.
solid answer
~40 sA Go slice value is a small header (pointer, length, capacity) passed by value, so `append` cannot change the caller's variable. It returns a header whose length is one greater, and when the existing capacity is exhausted it allocates a larger backing array first and returns a header pointing at *that*. Writing `append(s, x)` on its own is actually a compile error in Go, because the result of `append` may not be discarded as a statement. The same trap appears across a function boundary: `func add(buf []byte) { buf = append(buf, 'x') }` only rebinds the local parameter, so the caller sees nothing. Return the slice (or take a `*[]byte`) instead.
code
go · 5 liness := []int{1, 2, 3}
append(s, 4) // compile error: append(s, 4) evaluated but not used
s = append(s, 4) // correct: rebind s to the header append returnedgo deeper
Memorise the shape: always s = append(s, x). Be ready to say that a slice value is a header holding a pointer, a length and a capacity, and that append hands you a new header.
Explain both branches: spare capacity writes in place and bumps only the returned length; exhausted capacity allocates and copies. Say why the callee-rebinds-a-parameter bug compiles cleanly.
Show the judgment: an API that grows a caller's buffer should return the slice, the way strconv.AppendInt does, rather than take a pointer to one. Point out that capacity-dependent visibility is the reason this bug survives small tests.
Frame it as an interface rule for shared code: a function that takes a slice either fills a caller-sized window or returns the grown slice, and never both. Pin that in review so buffer ownership never has to be reasoned out case by case.
## What a slice value actually is A Go slice is not the data. It is a three-word descriptor: a pointer to the first element of a backing array, a length (how many elements you can index), and a capacity (how many elements fit between that pointer and the end of the array). Like every other value in Go, that descriptor is copied on assignment and copied when it is passed to a function. Nothing you do to your copy of the descriptor can be seen by anyone else's copy. ## What append does with it `append(s, x)` takes the descriptor `s` by value and produces a **new** descriptor. Two things can happen inside: - **There is spare capacity** (`len(s) < cap(s)`). append writes `x` into the slot just past the end, and returns a descriptor with the same pointer, the same capacity, and a length one greater. The element write is visible through the backing array, but the *new length* exists only in the returned descriptor. - **There is no spare capacity** (`len(s) == cap(s)`). append allocates a larger array, copies the existing elements into it, writes `x`, and returns a descriptor pointing at the new array. Your original `s` still points at the old array and knows nothing about `x`. Because the second case exists, the returned value is the only thing you can rely on. This is why the canonical form is always `s = append(s, x)`, and why every piece of Go you will ever read spells it that way. ## The compiler will not let you discard it Go's specification forbids using `append` as a bare expression statement (the same rule covers `len`, `cap`, `make`, `new` and friends). So: ``` append(s, 4) // compile error: append(s, 4) evaluated but not used s = append(s, 4) // fine ``` That rule catches the single-variable version of the mistake for free. It does **not** catch the interesting version. ## The version the compiler cannot catch ```go func addRecord(buf []byte, rec []byte) { buf = append(buf, rec...) } ``` This compiles cleanly and does nothing useful. `buf` inside the function is a copy of the caller's descriptor; rebinding it is a local assignment. Depending on capacity, the caller may or may not see the *bytes* land in the shared backing array, but the caller's length never moves, so the bytes are past the end of its slice and invisible. The two fixes are: return the slice and make the caller rebind it (`func addRecord(buf, rec []byte) []byte`), which is what the standard library does everywhere (`strconv.AppendInt`, `append`-style builders, `fmt.Appendf`); or take a pointer, `func addRecord(buf *[]byte, rec []byte)`, and write `*buf = append(*buf, rec...)`. The first is far more idiomatic; pointers to slices are a smell unless you are writing an accumulator that many callers mutate. ## The half-visible middle case The reason "sometimes it seems to work" is the spare-capacity path. Consider a pipeline stage that hands a scratch buffer to a helper: ``` buf := make([]byte, 0, 64) // len 0, cap 64 addRecord(buf, rec) // writes rec into buf's array, caller's len stays 0 ``` The bytes really were written into the array the caller owns. The caller simply cannot see them, because its length is still 0. Change the capacity to 0 and the helper allocates a fresh array instead, so not even the bytes are shared. Behaviour that flips on capacity is exactly the kind of bug that survives testing and fails on a bigger input, which is why the rule is stated absolutely: **always use the value append gives back.** ## What to say in an interview "Slices are value types wrapping a pointer, length and capacity. append may reallocate, so it has to return a new header; discarding it is a compile error for a local, and a silent bug across a function call. Return the slice."
- If append reallocated, what happened to the original backing array?Nothing was freed eagerly. The old array stays alive as long as some slice header still points into it, and becomes garbage once the last one is gone. The elements were copied into the new array, so the two arrays are independent from that moment on: writing through the old header no longer affects the new one.
- How would you write a helper that appends to a caller's slice without returning it?Take a pointer to the slice: `func addRecord(buf *[]byte, rec []byte) { *buf = append(*buf, rec...) }`. That works because the pointer lets you rebind the caller's header. It is legal but rarely idiomatic; the standard library's convention is the `Append`-style signature that takes a destination slice and returns the grown one.
- Does append(s, x) ever modify data the caller can already see?Yes, when `len(s) < cap(s)`. The new element is written into the backing array at index `len(s)`, which is inside the array the caller also references. Another slice over the same array with a larger length will see the write. That is the aliasing hazard behind reusing one buffer across a pipeline.
saying these in an interview costs you the question
- Claims append mutates the slice argument in place
- Says append(s, x) alone compiles and works
- Thinks a slice is a reference type like a map
- Believes appending inside a function is visible to the caller
- Assumes append always allocates a new array