18. Slices: the list you'll actually use

📖 Reading · 9 min
💡 Every code box below is live — edit it and hit Run.

A slice is Go's growable list. It looks like an array with the size left out, and that one missing number changes everything: slices resize, they're cheap to pass around, and every Go program is full of them.

Declaring a slice

package main

import "fmt"

func main() {
    names := []string{"Ada", "Grace", "Katherine"}
    fmt.Println(names, len(names))

    var empty []int
    fmt.Println(empty, len(empty), empty == nil)

    nums := []int{}
    fmt.Println(nums, len(nums), nums == nil)
}

[]string — no number between the brackets. That's the entire syntactic difference from an array, and it's how you tell them apart at a glance.

A var empty []int that's never assigned is nil. A nil slice is perfectly usable: it has length 0, you can range over it, and you can append to it. That's why var out []int is the idiomatic way to start collecting results — you don't need to initialise anything.

Indexing and iterating

Exactly like arrays:

package main

import "fmt"

func main() {
    langs := []string{"Go", "Rust", "Python"}

    fmt.Println(langs[0], langs[len(langs)-1])

    for i, lang := range langs {
        fmt.Printf("%d: %s\n", i, lang)
    }

    langs[1] = "Zig"
    fmt.Println(langs)
}

langs[len(langs)-1] is the idiom for "last element" — Go has no negative indexing.

Growing with append

This is the function you'll call more than any other:

package main

import "fmt"

func main() {
    var queue []string

    queue = append(queue, "first")
    queue = append(queue, "second")
    queue = append(queue, "third", "fourth")

    fmt.Println(queue, len(queue))

    more := []string{"fifth", "sixth"}
    queue = append(queue, more...)
    fmt.Println(queue, len(queue))
}

Two things to burn in:

  1. append returns a new slice header — you must assign the result. Writing append(queue, "x") on its own line and expecting queue to change is the single most common Go beginner bug. The compiler catches it (it complains the value is unused), but the habit matters.
  2. To append one slice to another, spread it with ... — the same ... you used for variadic functions, because append is variadic.

Building a slice from a loop

The everyday pattern:

package main

import "fmt"

func main() {
    var squares []int
    for n := 1; n <= 6; n++ {
        squares = append(squares, n*n)
    }
    fmt.Println(squares)

    var long []string
    for _, w := range []string{"go", "gopher", "concurrency", "map"} {
        if len(w) > 3 {
            long = append(long, w)
        }
    }
    fmt.Println(long)
}

Start nil, append as you go. Go doesn't have list comprehensions; it has this, and every Go programmer reads it instantly.

make — pre-sizing a slice

When you know how many elements you're about to produce, tell Go up front:

package main

import "fmt"

func main() {
    scores := make([]int, 3)
    fmt.Println(scores, len(scores))

    scores[0] = 90
    fmt.Println(scores)

    buf := make([]string, 0, 10)
    fmt.Println(buf, len(buf), cap(buf))
}

make([]int, 3) gives you a slice of three zeroes — the elements exist, so you assign by index rather than appending.

make([]string, 0, 10) is different: length 0 (nothing in it yet) but capacity 10 — room for ten before Go has to allocate again. It's the version you want when you'll append in a loop and know roughly how many. That cap is the subject of the next lesson.

Watch the trap: make([]int, 3) followed by append gives you five elements, not two — the first three are the zeroes you asked for.

Slices can't be compared with ==

package main

import "fmt"

func main() {
    a := []int{1, 2, 3}
    b := []int{1, 2, 3}

    // fmt.Println(a == b) // won't compile
    fmt.Println(a == nil, b == nil)

    equal := len(a) == len(b)
    for i := range a {
        if equal && a[i] != b[i] {
            equal = false
        }
    }
    fmt.Println("equal:", equal)
}

The only comparison a slice allows is against nil. For element-wise equality you loop (or call slices.Equal from the standard library, which you'll meet in the stdlib tour). Arrays compare, slices don't — that's the price of being growable.

Your turn

Given a slice of temperatures, build a new slice containing only the ones above freezing (> 0), then print it. The program should output exactly:

[12 5 30]
package main

import "fmt"

func main() {
    temps := []int{12, -3, 5, -20, 30, 0}
    // build `warm` with only the values greater than 0
    fmt.Println(warm)
}
package main

import "fmt"

func main() {
    temps := []int{12, -3, 5, -20, 30, 0}
    var warm []int
    for _, t := range temps {
        if t > 0 {
            warm = append(warm, t)
        }
    }
    fmt.Println(warm)
}

Next: what append is really doing underneath, and why cap matters.