Understanding Go Slice Length and Capacity

Slice Declaration

Unlike languages such as PHP, Go provides slices as a reference type that wraps an underlying array. Arrays in Go have fixed sizes, but slices offer dynamic behavior while referencing array elements.

/*
 * Array declaration
 */
var a [5]int // Length specified, elements default to 0
var a [5]int{1, 2, 3, 4, 5}

/*
 * Slice declaration - like declaring an array without length
 */
// No underlying array created
// Method 1: Direct initialization
var s []int // nil slice with length and capacity of 0
var s []int{1, 2, 3, 4, 5} // Creates a 5-element array

// Method 2: Using make() function
// var variableName = make([]elementType, length, capacity)
var s = make([]int, 0, 5)

// Underlying array already exists
// Slice with range: var variableName []elementType = arr[low, high]
var arr = [5]int{1, 2, 3, 4, 5}
var slice []int = arr[1:4] // Elements at indices 1, 2, 3: [2, 3, 4]

Length vs Capacity

The length of a slice represents the number of elements it currently contains. The capacity indicates the number of elements from the first element to the end of the underlying array. Use len() and cap() to retrieve these values:

s := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9} // [0 1 2 3 4 5 6 7 8 9] len=10, cap=10
s1 := s[0:5]  // [0 1 2 3 4] len=5, cap=10
s2 := s[5:]   // [5 6 7 8 9] len=5, cap=5

How append Modifies Length and Capacity

The append Function

Go provides the built-in append function to add elements to a slice:

func append(s []T, vs ...T) []T

The return value is a slice containing all original elements plus the new ones.

Example 1: Capacity remains unchanged

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4, 5}
    fmt.Println(arr)

    s1 := arr[0:3] // [1 2 3]
    printDetails(s1)
    s1 = append(s1, 6)
    printDetails(s1)
    fmt.Println(arr)
}

func printDetails(s []int) {
    fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}

Output:

[1 2 3 4 5]
len=3 cap=5 0xc000082030 [1 2 3]
len=4 cap=5 0xc000082030 [1 2 3 6]
[1 2 3 6 5]

After appending, the slice's capacity and memory address remain unchanged. The underlying array reflects the modification at index 3, where 4 became 6.

Example 2: Capacity doubles

package main

import "fmt"

func main() {
    arr := [5]int{1, 2, 3, 4}
    fmt.Println(arr)

    s2 := arr[2:] // [3 4 0]
    printDetails(s2)
    s2 = append(s2, 5)
    printDetails(s2)
    fmt.Println(arr)
}

func printDetails(s []int) {
    fmt.Printf("len=%d cap=%d %p %v\n", len(s), cap(s), s, s)
}

Output:

[1 2 3 4 0]
len=3 cap=3 0xc00001c130 [3 4 0]
len=4 cap=6 0xc00001c180 [3 4 0 5]
[1 2 3 4 0]

Here, both capacity and memory address changed, while the original array stayed intact.

When the underlying array lacks sufficient capacity, Go allocates a larger array and returns a slice pointing to this new allocation.

Internal Slice Structure

The slice data structure is defined in src/runtime/slice.go:

// go 1.3.16 src/runtime/slice.go:13
type slice struct {
    array unsafe.Pointer
    len   int
    cap   int
}

A slice is a struct containing the length, capacity, and a pointer to the underlying array. When appending exceeds capacity, the growslice function handles allocation:

// go 1.3.16 src/runtime/slice.go:76
func growslice(et *_type, old slice, cap int) slice {
    newcap := old.cap
    doublecap := newcap + newcap
    if cap > doublecap {
        newcap = cap
    } else {
        if old.len < 1024 {
            newcap = doublecap
        } else {
            for 0 < newcap && newcap < cap {
                newcap += newcap / 4
            }
            if newcap <= 0 {
                newcap = cap
            }
        }
    }

    var overflow bool
    var lenmem, newlenmem, capmem uintptr
    switch {
        // ...code
    }

    memmove(p, old.array, lenmem)

    return slice{p, old.len, newcap}
}

The growth rules are:

  • If capacity is under 1024 elements, it doubles on each growth
  • Once capacity exceeds 1024, growth factor becomes 1.25 (adding 25% per expansion)
  • If expansion stays within the original array's bounds, the slice still references the original array
  • If expansion exceeds the original array's capacity, a new array is allocated and data is copied—the original array remains unaffected

In both examples above, capacity was under 1024, so the growth factor was 2. In Example 2, the slice's underlying array lacked available space, so append() created a new array, copied existing values, and added the new one, leaving the original array untouched.

Visualizing Growth Scenarios

Scenario 1: Capacity unchanged after growth

slice := []int{1, 2, 3, 4, 5}
mySlice := slice[1:3]      // Length: 2, Capacity: 4
mySlice = append(mySlice, 40)

The resulting memory layout shows the slice utilizing existing capacity without triggering reallocation.

Scenario 2: New allocation after growth

mySlice := []int{1, 2, 3, 4, 5} // Length and capacity both 5
mySlice = append(mySlice, 6)

This triggers a new array allocation since the original capacity was exhausted.

Key Takeaways

  1. A slice is a struct containing capacity, length, and a pointer to an underlying array
  2. Setting initial capacity avoids growslice calls—new allocations create new memory addresses and copy data, impacting performance

Tags: Go slice length capacity append

Posted on Thu, 13 Aug 2026 16:51:07 +0000 by BoarderLine