Back to blog

Sunday, August 16, 2026

Understanding Go Slice Length vs Capacity (With Diagrams)

Title card for 'Understanding Go Slice Length vs Capacity (With Diagrams)' with a short code example

Length is how many elements you can index right now. Capacity is how many slots exist from the slice's first element to the end of its backing array. len(s) <= cap(s) always. s[i] panics if i >= len(s), even when i < cap(s). append fills unused capacity; only when that runs out does it allocate a new array.

The failure

Two declarations that look similar. They are not.

package main

import "fmt"

func main() {
    a := make([]int, 5)    // len 5, cap 5: five zeros
    b := make([]int, 0, 5) // len 0, cap 5: empty, room for five

    a = append(a, 9)
    b = append(b, 9)

    fmt.Println(a) // [0 0 0 0 0 9]
    fmt.Println(b) // [9]
}

People write make([]int, n) when they meant "room for n items." They get n zeros, then append puts the real data after those zeros.

The other panic:

s := make([]int, 3, 10)
fmt.Println(s[3]) // panic: index out of range [3] with length 3

Slot 3 exists in the array. The slice does not show it until you extend length (s = s[:4]) or append.

Why they differ

The header is a window over an array:

Slice header with ptr, len 3 and cap 6 pointing at a backing array of six ints, the first three inside len

make([]int, 3, 6)  →  array of 6 ints, window covers the first 3

index:   0    1    2    3    4    5
array: [ 0 ][ 0 ][ 0 ][ 0 ][ 0 ][ 0 ]
         |--- len=3 ---|
         |---------- cap=6 ---------|

Visible elements = length. Spare cells = cap - len. append writes those spare cells first.

Reslicing moves the window. It does not copy:

parent := []int{10, 20, 30, 40, 50}   // len=5 cap=5

child := parent[1:3]                  // [20 30]

index:      0     1     2     3     4
parent:  [ 10 ][ 20 ][ 30 ][ 40 ][ 50 ]
child:         [ 20 ][ 30 ]
                |-- len=2 --|
                |------ cap=4 ------|

cap(child) is 4 because counting starts at the child's pointer (index 1) and runs to the end of the array. A later append(child, 99) writes into parent's index 3 if you do not cap capacity.

You can shorten length without shrinking capacity:

s := []int{2, 3, 5, 7, 11, 13}
s = s[:0]
// len=0 cap=6: the six values are still in the array
s = s[:4]
// len=4 cap=6: [2 3 5 7] again

You cannot lengthen past capacity. s[:cap(s)+1] panics.

When append exceeds capacity, Go allocates a larger array (often doubling while small) and copies. The old array stays alive only if some other header still points at it.

before:  ptr → [1][2][3]     len=3 cap=3
append 4:
after:   ptr → [1][2][3][4][?][?]   len=4 cap=6   (new array)
         old → [1][2][3]            (orphaned unless another slice holds it)

The fix

Name the two numbers when you allocate:

// Empty, with room. Use this before a loop of append.
buf := make([]int, 0, n)

// Already n zeros, assign by index.
out := make([]int, n)
for i := range src {
    out[i] = src[i] * 2
}

Print both when debugging: fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s).

Cap a subslice when it must not grow into a neighbor: s[low:high:high].

Copy a small window out of a huge buffer so capacity (and the huge array) does not leak into long-lived state, that is the next article.