skip to content

The Slice Header

A slice is a small value holding a pointer, a length and a capacity, so copying it copies the header and not the elements. That is why a function can change your elements but not your length.

part ofGo (Golang)overview, primer and where to startread it →
on this pageshow

questions

4

What three fields does a Go slice header hold at runtime, and what does each mean?

level: juniorimportance: must knowfreq 78%

answer

  1. the elements live somewhere else
  2. a fixed-size value, three words wide
  3. one address plus two integers
  4. usable now versus room already there

basics

~20 s

A Go slice value is three words: a pointer to the first element it covers in a backing array, a length, and a capacity. The elements live in that array, not inside the slice value.

solid answer

~40 s

A slice value is a three-word header: a pointer to the first element it covers in a backing array, an `int` length, and an `int` capacity. `len(s)` bounds indexing; `cap(s)` counts how many elements exist from that pointer to the end of the backing array, so re-slicing up to `cap` needs no new allocation. Capacity is measured from the slice's own pointer, not from the array's start, which is why `r[2:5]` on a length-8, capacity-8 slice has length 3 and capacity 6. The header is a fixed size regardless of how many elements it covers - `unsafe.Sizeof(s)` reports 24 on a 64-bit build. An array `[16]float64` is the opposite: it *is* its sixteen elements, 128 bytes, with no indirection at all.

code

go · 9 lines
go
scores := make([]float64, 3, 8)
// 3 8 - three usable elements inside an array of eight
fmt.Println(len(scores), cap(scores))
// 24 on a 64-bit build: pointer + len + cap, whatever len is
fmt.Println(unsafe.Sizeof(scores))

var fixed [16]float64
// 128 - an array value IS its sixteen elements
fmt.Println(unsafe.Sizeof(fixed))

go deeper

for a junior

Be ready to name the three words out loud - pointer, length, capacity - and to say that the elements live in a backing array the header points at, not inside the slice value itself.

for a middle

Expect to compute length and capacity after a re-slice such as r[2:5], and to explain that capacity is measured from the slice's own pointer to the end of the backing array, never from the array's start.

for a senior

Reason about cost and sharing from the header: passing a slice moves three words while passing a [16]float64 copies every element, and two headers over one array means writes travel between them.

for a principal

Own the API consequence. Whether a package exposes fixed-size arrays or slices decides whether callers get independent data or a shared view, and that is hard to reverse once other teams depend on the signature.

## Array versus slice: what the value contains Go draws a hard line between an array and a slice, and the line is about what the value itself holds. An **array** type is written `[N]T`, and the length is part of the type. `[16]float64` is a value that *is* sixteen float64s laid out end to end - 128 bytes. Assign it, pass it to a function, or embed it in a struct, and all 128 bytes are copied. There is no indirection anywhere. A **slice** type is written `[]T`, with no length in the type. A slice value is *not* the elements. It is a small, fixed-size **header** of exactly three machine words: - **a pointer** to the first element the slice covers, somewhere inside a backing array; - **a length** (`int`) - how many elements you may index; - **a capacity** (`int`) - how many elements exist from that pointer to the end of the backing array. `make([]float64, 3, 8)` allocates a backing array of eight float64s and hands you a header whose pointer names element 0, whose length is 3 and whose capacity is 8. ## What len and cap each bound `len(s)` bounds indexing: `s[i]` panics unless `0 <= i < len(s)`. `cap(s)` bounds **re-slicing**: `s[a:b]` is legal for `0 <= a <= b <= cap(s)`. Capacity is therefore spare room that already exists in the backing array beyond the slice's current end. The detail people get wrong is that capacity is counted **from the slice's own pointer**, not from the start of the backing array. If `r` has length 8 and capacity 8, then ``` w := r[2:5] ``` gives `len(w) == 3` and `cap(w) == 6`. The new header's pointer moved forward two elements, so only six remain between it and the array's end. The two elements behind the window are simply unreachable through `w`: a slice can never look backwards, because the header has no way to express a negative offset. ## The header is a fixed size `unsafe.Sizeof(s)` on any slice reports 24 on a 64-bit build (three 8-byte words) and 12 on a 32-bit build - whether the slice covers zero elements or a million. That is exactly why passing a slice to a function is cheap while passing a large array is not: the slice costs three words on the stack, the `[16]float64` costs 128 bytes copied. This also explains a common surprise: a struct containing a `[]byte` is small, no matter how much data that `[]byte` describes, and `unsafe.Sizeof` will tell you so. Sizeof measures the value, never what it points at. ## The zero value, and nil versus empty `var s []float64` is a **nil slice**: pointer nil, length 0, capacity 0. It is perfectly usable - `len(s)` is 0, ranging over it runs zero times, and `append` works on it. `[]float64{}` is a *different* value: a non-nil pointer, length 0, capacity 0. Both have length 0, and code that only reads or ranges cannot tell them apart. They differ where nilness is observable: `s == nil` is true for one and false for the other, and `encoding/json` marshals a nil slice as `null` but an empty non-nil slice as `[]`. That is a real API difference for a library that returns "no results". ## Two headers, one array Because the header carries a pointer, several slices can describe overlapping regions of one backing array. `r`, `r[2:5]` and `r[:4]` are three independent header values sharing one allocation, and a write through any of them into an overlapping index is visible through the others. This is the single most important consequence of the three-word design, and it is where most slice bugs come from: the header is copied freely, the elements never are. ## Reading the header Ordinary Go gives you `len` and `cap`; the pointer word is not directly readable through the safe language. `unsafe.SliceData(s)` returns it as a typed `*T` (added in Go 1.20). Older code cast a slice to `*reflect.SliceHeader` to read `Data`, `Len` and `Cap` as struct fields; that type still exists but is deprecated, and building one by hand was always unsound. In practice the easiest way to see the header is a debugger. Stop on a function that takes a `[]float64` and the parameter expands into its three fields, and comparing the data pointers of two supposedly independent slices is the fastest possible proof that they alias. ## Why it is built this way The design buys two things at once. An array gives you value semantics and a compile-time length, so it is comparable (if `T` is), can key a map, and is genuinely independent when copied. A slice gives you a cheap, copyable *view* over storage that can be re-sliced and grown, at the cost of one indirection and the sharing that comes with it. Knowing which of the two you are holding - three words, or the whole payload - answers most questions about what a piece of Go code will cost and what it will share.

  • For a slice `r` with length 8 and capacity 8, what are `len(r[2:5])` and `cap(r[2:5])`?
    Length 3 and capacity 6. The length is the difference between the bounds, 5 minus 2. The capacity is counted from the new header's pointer, which has moved forward two elements, so six of the original eight remain between it and the end of the backing array. The two elements before index 2 are unreachable through the new slice.
  • Does the size of a slice value grow as it covers more elements?
    No. The header is always three machine words - 24 bytes on a 64-bit build - because it stores a pointer and two ints. `unsafe.Sizeof` on a slice covering a million elements still reports 24. Only the separately allocated backing array grows. That is why passing a slice is cheap while passing a large array copies every element.
  • How does a nil slice's header differ from that of an empty non-nil slice?
    A nil slice, `var s []int`, has a nil pointer, length 0 and capacity 0. `[]int{}` has a non-nil pointer, length 0 and capacity 0. Reading, ranging and appending behave identically on both. They differ only where nilness is visible: `s == nil`, and `encoding/json` writes `null` for the nil one and `[]` for the empty one.

A slice header is a library index card: which shelf position your books start at, how many are on loan to you, and how many spaces remain to the end of that shelf. Copying the card does not copy the books.

saying these in an interview costs you the question

  • Says a slice value stores its own elements inline
  • Treats len and cap as interchangeable
  • Claims the slice header grows as elements are added
  • Says cap counts from the backing array's first element
  • Thinks [16]float64 and []float64 are the same kind of value
open as a page

Why can a Go function mutate a slice's elements but not change the caller's length?

level: middleimportance: must knowfreq 72%

basics

~20 s

A slice is passed by value, so the callee gets its own copy of the three-word header. Writes through the shared pointer reach the caller's elements; assigning a new length or capacity only changes the callee's copy.

open as a page

Why does returning a Go struct by value still share its []float64 field between callers?

level: seniorimportance: should knowfreq 48%

basics

~20 s

Copying a struct copies a slice field's three-word header, not the elements. Both copies hold the same backing-array pointer, so a write through one is seen by the other. An array field like [16]float64 is copied element by element instead.

open as a page

Why should you use unsafe.SliceData instead of casting a slice to *reflect.SliceHeader?

level: seniorimportance: nice to knowfreq 26%

basics

~20 s

reflect.SliceHeader stores its Data field as a uintptr, which the garbage collector does not trace, so a hand-built header can name an already-collected array. unsafe.SliceData returns a typed pointer that keeps the backing array alive.

open as a page