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.