Go Slice append May Reuse or Replace the Backing Array
Predict Go slice aliasing with length and capacity, limit append with full slice expressions, and copy when storage must be independent.
append reuses a slice’s backing array when its capacity can hold the new elements; otherwise it allocates another array and returns a slice referring to that storage. Always use the returned slice. If two slices might share storage, decide explicitly whether element changes and future appends should be visible across both views.
These rules are current in Go 1.27 and are long-standing language semantics. The allocation size and capacity growth strategy are implementation details, so code must not depend on a particular sequence of capacity values.
A slice is a view, not an array
The Go specification describes a slice value as a view onto an underlying array. It has a length, a capacity, and a reference to storage. Length controls which elements are currently addressable. Capacity measures how far that view can be extended from its first element.
Copying a slice variable copies that descriptor, not its elements:
a := []int{10, 20, 30}
b := a[:2]
b[0] = 99
fmt.Println(a[0]) // 99
Both views refer to the same array element. This aliasing is independent of append; ordinary element assignment is visible through every overlapping view.
Capacity decides what append may reuse
The specification’s append rule is binary: insufficient capacity requires a new sufficiently large array; sufficient capacity reuses the existing array. This complete example demonstrates both paths:
package main
import "fmt"
func main() {
base := make([]int, 2, 4)
base[0], base[1] = 10, 20
shared := append(base, 30)
fmt.Println(base[:3])
limited := base[:2:2]
independent := append(limited, 40)
independent[0] = 99
fmt.Println(base[:3])
fmt.Println(independent)
}
It prints:
[10 20 30]
[10 20 30]
[99 20 40]
The first append fits within base’s capacity, so shared and base still refer to the same array. base retains length two, but reslicing it to length three reveals the appended value.
The full slice expression base[:2:2] sets the result’s capacity to two. Appending one element cannot fit, so it allocates. Changing independent[0] then leaves base untouched.
A full slice expression limits growth, not mutation
The form s[low:high:max] sets length to high-low and capacity to max-low. The specification’s full slice expression rules require low <= high <= max <= cap(s).
Clipping capacity is a useful ownership signal when handing a subslice to code that may append:
func header(packet []byte) []byte {
return packet[:8:8]
}
Appending to the returned eight-byte view must allocate. However, assigning view[0] still changes packet[0], because the existing elements share storage. Capacity restriction is not isolation. Make a copy when mutation must also be independent.
The standard library’s slices.Clip similarly removes unused capacity without copying the current elements. It prevents an in-capacity append through that view, but it does not sever the backing-array relationship.
Functions must return the appended slice
A slice descriptor is passed by value. A callee can modify shared elements, but assigning a new descriptor to its parameter does not update the caller’s slice variable:
func addWrong(values []int, value int) {
values = append(values, value)
}
func add(values []int, value int) []int {
return append(values, value)
}
values = add(values, 3)
addWrong may write into unused capacity, making a value exist in the backing array while the caller’s length remains unchanged. Or it may allocate an array that becomes unreachable when the function returns. In neither case does the caller receive the new slice length. APIs that append should return the result, accept a pointer to a slice only when descriptor mutation is truly part of the contract, or own the slice internally.
Copy when the boundary requires ownership
Use copy, slices.Clone, or an append-to-nil idiom to create independent storage:
owned := append([]byte(nil), packet[start:end]...)
For a non-empty range, this allocates storage and copies the bytes. slices.Clone expresses the same intent for any slice type. The copy is shallow: if elements are pointers, maps, slices, or other reference-bearing values, the referenced objects may still be shared.
Copying has allocation and bandwidth costs. It is worth paying at boundaries where a producer will reuse a buffer, a caller might retain data beyond a callback, or concurrent owners need independent mutation. When an API intentionally lends a view, document how long the view remains valid and whether the caller may change it.
Small subslices can retain large arrays
A live slice keeps its backing array reachable. Returning a ten-byte subslice of a multi-megabyte buffer can therefore retain the entire array. The official Go slices article shows this retention problem and fixes it by copying the needed data before returning.
This is not a leak in the garbage collector: the array remains reachable by design. Copy the small result when its lifetime is much longer than the large source and retaining the source materially affects memory. Avoid copying reflexively when both views share the same short lifetime.
Overlap is defined, growth size is not
The built-in copy works even when source and destination overlap. append also has defined element results when its source arguments overlap the destination’s storage. These guarantees make in-place deletion and shifting patterns possible.
What is not specified is the capacity chosen after allocation. A runtime may change its growth policy between releases or based on element size. Preallocate with make([]T, 0, expected) when an estimate avoids repeated growth, but treat the estimate as a performance choice rather than a correctness requirement.
When debugging an aliasing surprise, inspect four facts: the two slice ranges, whether their arrays overlap, each slice’s capacity, and whether the relevant append result was assigned. Length tells you what is visible now; capacity tells you whether growth can remain shared; only a copy guarantees independent element storage.