t0ul

Automate the Boring Stuff with Go, Ch. 5: Slices

Part of Automate the Boring Stuff with Go, a beginner-friendly guide to programming in Go.

Updated for Go 1.27 in October 2026. What changed: the slices package, the min and max builtins, what a sub-slice really is, and range over an integer.

A slice is Go’s growable list. An array is the first data structure in any programming book; a slice is an array that can grow, and it is what you use nearly all the time. Slices can hold any type, including other slices, which makes grids and tables easy. In this chapter we’ll go over the basics and the handful of operations you will use every day. The code is in the repo under ch5/examples.

The slice type

A slice expression has a lower and an upper bound separated by a colon. The upper bound is not included.

// ex. 1 slicing an array
package main

import "fmt"

func main() {
	evens := [5]int{2, 4, 6, 8, 10}
	s := evens[1:4] // from index 1 up to, but not including, index 4
	fmt.Println(s)
}

prints [4 6 8]: index 1, 2 and 3 of the array, stopping before index 4.

Accessing values by index

As in many languages, you get one element with square brackets: evens[0] is 2. Counting starts at zero.

Slices of slices

A slice can hold slices, which gives you a grid. Tic-tac-toe is the friendly version of the x,y coordinate plane:

// ex. 2 a slice of slices: tic-tac-toe
package main

import (
	"fmt"
	"strings"
)

func main() {
	board := [][]string{
		{"_", "_", "_"},
		{"_", "_", "_"},
		{"_", "_", "_"},
	}

	// the players take turns
	board[0][0] = "X"
	board[2][2] = "O"
	board[1][2] = "X"
	board[1][0] = "O"
	board[0][2] = "X"

	for _, row := range board {
		fmt.Println(strings.Join(row, " "))
	}
}

Which prints:

X _ X
O _ X
_ _ O

Empty slices

When you don’t know how many elements you’ll have but you know you’ll have some, start empty:

// ex. 3 an empty slice, waiting to be filled
package main

import "fmt"

func main() {
	a := []int{}
	fmt.Println(a, len(a))
}

prints [] 0.

Appending

Adding elements is what append is for. Note the s = append(s, ...): append returns the grown slice and you keep it.

// ex. 4 append grows a slice
package main

import "fmt"

func main() {
	var s []int
	printSlice(s)

	// append works on nil slices
	s = append(s, 0)
	printSlice(s)

	// the slice grows as needed
	s = append(s, 1)
	printSlice(s)

	// more than one element at a time
	s = append(s, 2, 3, 4)
	printSlice(s)
}

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

len is how many elements are in the slice; cap is how many fit before Go has to allocate a bigger array behind the scenes. You will rarely care about cap, but now the output makes sense.

Length

// ex. 5 how many elements
package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	fmt.Println(len(s))
}

Changing a value by index

// ex. 6 change one value by index
package main

import "fmt"

func main() {
	s := []int{2, 3, 5, 7, 11, 13}
	s[1] = 234234
	fmt.Println(s)
}

prints [2 234234 5 7 11 13].

Using slices

With the basics down, let’s see how and why we’d use a slice to automate something.

Loops

Loops and slices go together like milk and cookies, and Go’s range walks a slice element by element.

Suppose you sell paper, have a list of prices, and need to raise them all for inflation:

// ex. 7 range over a slice
package main

import "fmt"

func main() {
	paperPrices := []float64{1.00, 2.00, 4.00, 8.00, 16.00, 32.00, 64.00, 128.00}
	inflated := make([]float64, len(paperPrices))
	for i, v := range paperPrices {
		inflated[i] = v * 1.08
	}
	fmt.Println(inflated)
}

make([]float64, len(paperPrices)) creates a slice of a given length up front, filled with zeros; here we know the exact size we need.

The loop variables i and v exist only inside the loop and can be named anything, but i for index and v for value are the tradition. If you only need the value, write for _, v := range; the underscore means “I’m ignoring this one”.

range is a shortcut for the longer loop you met in chapter 3:

// ex. 8 the same loop, the long way
package main

import "fmt"

func main() {
	paperPrices := []float64{1.00, 2.00, 4.00, 8.00, 16.00, 32.00, 64.00, 128.00}
	inflated := make([]float64, len(paperPrices))
	for i := 0; i < len(paperPrices); i++ {
		inflated[i] = paperPrices[i] * 1.08
	}
	fmt.Println(inflated)
}

The slices package

Searching, sorting and finding the biggest value used to mean writing the loop yourself. Since Go 1.21 the standard library’s slices package has them, and they work for a slice of any type:

// ex. 9 the slices package and the min/max builtins
package main

import (
	"fmt"
	"slices"
)

func main() {
	temps := []int{71, 64, 80, 58, 77}

	fmt.Println(slices.Contains(temps, 80)) // true
	fmt.Println(slices.Index(temps, 58))    // 3
	fmt.Println(slices.Max(temps), slices.Min(temps))

	sorted := slices.Clone(temps) // Sort works in place, so sort a copy
	slices.Sort(sorted)
	fmt.Println(sorted)

	slices.Reverse(sorted)
	fmt.Println(sorted)

	// min and max are builtins for two or more values of the same type
	fmt.Println(min(3, 9, 1), max(2.5, 1.5))
}

slices.Sort sorts in place, so if you still need the original order, sort a slices.Clone. min and max are builtin functions that take two or more values; you do not import anything for them.

A sub-slice shares memory

This one surprises everybody once. A slice is a window onto an array. Slicing a slice makes a new window onto the same array, so writing through one is visible through the other:

// ex. 10 a sub-slice shares memory with its parent
package main

import (
	"fmt"
	"slices"
)

func main() {
	week := []string{"Mon", "Tue", "Wed", "Thu", "Fri"}

	midweek := week[1:4] // looks separate, but points into the same array
	midweek[0] = "TUESDAY"
	fmt.Println(week) // week changed too

	independent := slices.Clone(week[1:4]) // a real copy
	independent[0] = "tue"
	fmt.Println(week) // unchanged this time
	fmt.Println(independent)
}

When you want an independent copy, say so with slices.Clone. Keep this in mind in chapter 9, where rows read from a spreadsheet are slices too.

Range over an integer

Not a slice, but it belongs with range: since Go 1.22 you can range over a number to count from zero.

// ex. 11 range over an integer
package main

import "fmt"

func main() {
	for i := range 3 {
		fmt.Println("round", i+1)
	}
}

Conclusion

Slices, append, range, and the slices package cover most list work you will ever do. Next up: maps, for when you want to look things up by name instead of by position.

← Back to the blog