Explain arrays vs. slices — how do slices work under the hood?

5 minbeginnergoslicesarraysfundamentals

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)