Go by Example

Slices

Dynamic sequences built on arrays - make, literals, append, copy, sub-slicing, and the len vs cap distinction.

A slice is a view into an underlying array. It has three fields: a pointer to the array, a length, and a capacity. Slices are the workhorse collection in Go - dynamic, passed by reference semantics, and composable.

Create a slice with make([]T, length, capacity) or with a slice literal. Both are nil-free and immediately usable. len returns the number of elements; cap returns the underlying array's size from the slice's start.

package main
 
import "fmt"
 
func main() {
    // Slice literal
    s := []int{1, 2, 3}
    fmt.Println(s, len(s), cap(s)) // [1 2 3] 3 3
 
    // make: length 3, capacity 5
    t := make([]int, 3, 5)
    fmt.Println(t, len(t), cap(t)) // [0 0 0] 3 5
}

append adds elements to a slice. If the slice has spare capacity, it appends in-place and returns a slice with a larger length. If capacity is exhausted, it allocates a new backing array, copies existing elements, and returns a slice pointing to the new array.

package main
 
import "fmt"
 
func main() {
    s := make([]int, 0, 3)
 
    for i := 0; i < 5; i++ {
        s = append(s, i)
        fmt.Printf("len=%d cap=%d %v\n", len(s), cap(s), s)
    }
    // len=1 cap=3 [0]
    // len=2 cap=3 [0 1]
    // len=3 cap=3 [0 1 2]
    // len=4 cap=6 [0 1 2 3]   ← new backing array allocated
    // len=5 cap=6 [0 1 2 3 4]
}

A sub-slice s[low:high] shares the same backing array as the original. Writes through the sub-slice mutate the original. Use copy to get an independent slice.

package main
 
import "fmt"
 
func main() {
    orig := []int{1, 2, 3, 4, 5}
 
    // Sub-slice shares backing array
    sub := orig[1:4]   // [2 3 4]
    sub[0] = 99
    fmt.Println(orig)  // [1 99 3 4 5] - original mutated!
 
    // Independent copy
    dst := make([]int, len(orig))
    copy(dst, orig)
    dst[0] = 0
    fmt.Println(orig)  // [1 99 3 4 5] - original untouched
    fmt.Println(dst)   // [0 99 3 4 5]
}

In production

Two slices sharing a backing array is the most common source of subtle mutation bugs in Go. A sub-slice write reaches through to the parent. In high-throughput services, pre-allocate with make([]T, 0, n) when you know the approximate size - it avoids repeated allocations and the GC pressure that comes with them. append doubling capacity on each overflow sounds cheap, but 10 000 appends to an unallocated slice trigger ~14 allocations and copies; a single make with a sensible capacity eliminates all of them.

Enjoyed this? Get more essays on software craft delivered to your inbox.

Subscribe free