Explain arrays vs. slices — how do slices work under the hood?
Quick Answer
An array ([5]int) has a fixed size that's part of its type; [5]int and [10]int are different types. A slice ([]int) is a small struct with three fields: a pointer to an underlying array, a length, and a capacity. Slicing an array or another slice doesn't copy data — it creates a new slice header pointing into the same backing array. This is why two slices can share memory, and why modifying one through its indices can affect the other. append grows a slice by allocating a new, larger backing array and copying data over only when capacity is exceeded; otherwise it writes in place.
Detailed Answer
Arrays are rare in everyday Go code. Slices are what you actually use.
The slice header
A slice value is really this, conceptually:
type sliceHeader struct {
ptr *T
len int
cap int
}
arr := [5]int{1, 2, 3, 4, 5}
s := arr[1:3] // len=2, cap=4 (from index 1 to end of arr)
fmt.Println(s) // [2 3]
s[0] = 99
fmt.Println(arr) // [1 99 3 4 5] — s shares arr's backing array
What happens on append
s := make([]int, 2, 2) // len=2, cap=2
s = append(s, 3) // cap exceeded: new backing array allocated, data copied
If len(s) < cap(s), append writes into the existing backing array. If not, Go allocates a new array (usually doubling capacity for smaller slices) and copies everything over. This is why appending to a slice sometimes affects a second slice sharing its backing array, and sometimes doesn't — it depends on whether that specific append triggered reallocation.
The classic slicing bug
func trimFirst(s []int) []int {
return s[1:] // still shares the same backing array as the caller's slice
}
Sub-slicing never copies. If you need an independent copy, use copy() explicitly:
dst := make([]int, len(src))
copy(dst, src)