Slices are how Go stores lists of things: lines from a file, numbers to sum, users to greet. By the end of this lesson you’ll be able to create slices, grow them with append, cut pieces out of them, sort and search them with the slices package, and avoid the one surprise that catches every Go beginner.
Slices are built on top of arrays, so I’ll start there, briefly.
Arrays: a fixed number of values
An array holds a fixed number of values of one type. The length is written in square brackets before the type, and it never changes.
package main
import "fmt"
func main() {
var a [3]int
fmt.Println(a, len(a))
b := [3]string{"go", "is", "fun"}
fmt.Println(b[0], b[2])
c := [...]int{10, 20, 30, 40}
fmt.Println(len(c))
}Code language: Go (go)
[0 0 0] 3
go fun
4Code language: plaintext (plaintext)
A few things to notice. An array you don’t fill in gets the zero value for every element (you met zero values in Variables, types and pointers). You read and write elements with b[0], and indexes start at 0. len gives the length. With [...] the compiler counts the elements for you.
The length is part of the type: [3]int and [4]int are different types, and you can’t assign one to the other.
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
var b [4]int
b = a
fmt.Println(b)
}Code language: Go (go)
./main.go:8:6: cannot use a (variable of type [3]int) as [4]int value in assignmentCode language: plaintext (plaintext)
Arrays are values
Assigning an array, or passing it to a function, copies every element. Changing the copy leaves the original alone:
package main
import "fmt"
func main() {
a := [3]int{1, 2, 3}
b := a
b[0] = 100
fmt.Println(a)
fmt.Println(b)
}Code language: Go (go)
[1 2 3]
[100 2 3]Code language: plaintext (plaintext)
Because of the fixed length, you’ll rarely use arrays directly. They’re handy for things that really have a fixed size (the 16 bytes of a UUID, the 3 parts of an RGB color), but for almost everything else you’ll use slices.
Slices: a list that can grow
A slice type looks like an array type without the length: []int, []string. You create one with a slice literal:
package main
import "fmt"
func main() {
primes := []int{2, 3, 5, 7}
fmt.Println(primes, len(primes))
primes[0] = 1
fmt.Println(primes[0], primes[len(primes)-1])
for i, p := range primes {
fmt.Println(i, p)
}
}Code language: Go (go)
[2 3 5 7] 4
1 7
0 1
1 3
2 5
3 7Code language: plaintext (plaintext)
Indexing and range work just like they do for arrays (range was covered in The for loop). Reading or writing past the end is not a compile error but a crash at run time: primes[10] stops the program with panic: runtime error: index out of range [10] with length 4.
What a slice really is
A slice doesn’t hold its elements itself. It’s a small value with three parts:
- a pointer to an element in an array somewhere in memory (the backing array),
- a length: how many elements the slice shows (
len), - a capacity: how many elements there are from that pointer to the end of the backing array (
cap).
So a slice is a window onto an array. You can see this by slicing an array yourself with arr[low:high], which gives you the elements from index low up to, but not including, high:
package main
import "fmt"
func main() {
arr := [5]int{10, 20, 30, 40, 50}
s := arr[1:4]
fmt.Println(s, len(s), cap(s))
s[0] = 99
fmt.Println(arr)
}Code language: Go (go)
[20 30 40] 3 4
[10 99 30 40 50]Code language: plaintext (plaintext)
The slice s shows 3 elements, but its capacity is 4 because the array continues one more element past the window. And writing s[0] changed arr[1]: there’s only one array, and the slice is looking at it. When you write a slice literal like []int{2, 3, 5, 7}, Go creates the backing array for you behind the scenes.
This also means passing a slice to a function is cheap. Only the three-part header is copied, not the elements, and the function can change the elements the caller sees.
Making a slice with make
When you know how many elements you need but not their values yet, use make. It takes the type, the length and, optionally, the capacity:
package main
import "fmt"
func main() {
a := make([]int, 3)
fmt.Println(a, len(a), cap(a))
b := make([]string, 0, 10)
fmt.Println(b, len(b), cap(b))
}Code language: Go (go)
[0 0 0] 3 3
[] 0 10Code language: plaintext (plaintext)
a has three zeroes, ready to be overwritten. b is empty but has room for 10 elements, which is useful when you’re about to add elements one by one and know roughly how many there will be.
Growing a slice with append
append adds elements to the end of a slice and returns the new slice. You always assign the result back:
package main
import "fmt"
func main() {
var names []string
names = append(names, "Ana")
names = append(names, "Ben", "Cleo")
more := []string{"Dev", "Eli"}
names = append(names, more...)
fmt.Println(names, len(names))
}Code language: Go (go)
[Ana Ben Cleo Dev Eli] 5Code language: plaintext (plaintext)
You can append one element, several, or a whole other slice by writing ... after it. Forgetting to use the result is a compile error, which is good, because the result is the only place the new length lives:
package main
import "fmt"
func main() {
nums := []int{1, 2}
append(nums, 3)
fmt.Println(nums)
}Code language: Go (go)
./main.go:7:2: append(nums, 3) (value of type []int) is not usedCode language: plaintext (plaintext)
How append grows the backing array
If the backing array still has room (length is less than capacity), append puts the new element in the next free spot. If it’s full, append allocates a bigger array, copies the elements over, and returns a slice that points to the new array. You can watch the capacity jump:
package main
import "fmt"
func main() {
var s []int
prev := -1
for i := range 20 {
s = append(s, i)
if cap(s) != prev {
fmt.Println("len", len(s), "cap", cap(s))
prev = cap(s)
}
}
}Code language: Go (go)
len 1 cap 4
len 5 cap 8
len 9 cap 16
len 17 cap 32Code language: plaintext (plaintext)
The array grows in bigger and bigger steps, so appending in a loop stays fast. The exact numbers are an implementation detail that can differ between Go versions, element types and sizes, so don’t write code that depends on them.
Slicing a slice
The [low:high] syntax works on slices too. Leave out low to start at 0, leave out high to go to the end:
package main
import "fmt"
func main() {
letters := []string{"a", "b", "c", "d", "e"}
fmt.Println(letters[1:3])
fmt.Println(letters[:2])
fmt.Println(letters[3:])
fmt.Println(letters[:])
}Code language: Go (go)
[b c]
[a b]
[d e]
[a b c d e]Code language: plaintext (plaintext)
A handy way to read s[1:3]: start at index 1, stop before index 3, so you get 3 - 1 = 2 elements. Slicing never copies elements. The new slice is another window onto the same backing array, and that leads to the classic surprise.
Look at this program and guess what the three lines print before you read the output:
package main
import "fmt"
func main() {
original := []int{1, 2, 3, 4, 5}
part := original[1:3]
part[0] = 20
fmt.Println(original)
part = append(part, 30)
fmt.Println(original)
fmt.Println(part)
}Code language: Go (go)
[1 20 3 4 5]
[1 20 3 30 5]
[20 3 30]Code language: plaintext (plaintext)
The first line is no surprise if you remember that part is a window onto original‘s array. The second one surprises everybody once. part has length 2 but capacity 4, so append found room in the backing array and wrote 30 into the next slot. That slot is also original[3], so the 4 in original was silently replaced.
Once an append needs more room than the capacity, Go moves the slice to a new array and the link is broken. From then on, changes to one slice no longer show up in the other:
package main
import "fmt"
func main() {
original := []int{1, 2, 3}
grown := append(original, 4) // no room: grown gets a new array
grown[0] = 100
fmt.Println(original)
fmt.Println(grown)
}Code language: Go (go)
[1 2 3]
[100 2 3 4]Code language: plaintext (plaintext)
Whether two slices share an array therefore depends on capacity, which you usually aren’t tracking. My rule: if you take a piece of a slice and plan to modify it or append to it while the original is still in use, make a real copy first. The next section shows how.
There’s also a three-index form, s[low:high:max], that limits the capacity of the new slice to max - low. With part := original[1:3:3] the capacity is 2, so the first append is forced to allocate a new array and can’t overwrite original. You’ll see it in library code now and then.
Copying a slice
The built-in copy(dst, src) copies elements from one slice into another and returns how many it copied. It copies as many as fit, which is the smaller of the two lengths:
package main
import "fmt"
func main() {
src := []int{1, 2, 3}
dst := make([]int, len(src))
n := copy(dst, src)
dst[0] = 100
fmt.Println(n, src, dst)
small := make([]int, 2)
fmt.Println(copy(small, src), small)
}Code language: Go (go)
3 [1 2 3] [100 2 3]
2 [1 2]Code language: plaintext (plaintext)
Note that copy never grows dst. If you just want an independent duplicate of a whole slice, slices.Clone from the standard library does the make and the copy in one call: part := slices.Clone(original[1:3]).
The slices package
The standard library’s slices package has the everyday operations you’d otherwise write loops for. Here are the ones I use most:
package main
import (
"fmt"
"slices"
)
func main() {
scores := []int{42, 7, 19, 88, 7}
fmt.Println(slices.Contains(scores, 19))
fmt.Println(slices.Index(scores, 7))
fmt.Println(slices.Index(scores, 100))
fmt.Println(slices.Max(scores), slices.Min(scores))
slices.Sort(scores)
fmt.Println(scores)
slices.Reverse(scores)
fmt.Println(scores)
fmt.Println(slices.Equal([]int{1, 2}, []int{1, 2}))
}Code language: Go (go)
true
1
-1
88 7
[7 7 19 42 88]
[88 42 19 7 7]
trueCode language: plaintext (plaintext)
Containsreports whether a value is in the slice.Indexreturns the index of the first match, or-1if there is none.MaxandMinreturn the largest and smallest element. They panic on an empty slice, so checklenfirst if it could be empty.SortandReversechange the slice in place. They don’t return anything.Equalreports whether two slices have the same length and the same elements in the same order.
These work for slices of numbers, strings and other comparable types. slices.Sort on a []string sorts alphabetically (byte by byte, so uppercase letters come before lowercase). How one function can work with many element types is the topic of Generics; you don’t need to know that to use them.
You need slices.Equal because == doesn’t work on slices. The compiler only lets you compare a slice to nil:
package main
import "fmt"
func main() {
a := []int{1, 2}
b := []int{1, 2}
fmt.Println(a == b)
}Code language: Go (go)
./main.go:8:14: invalid operation: a == b (slice can only be compared to nil)Code language: plaintext (plaintext)
The min and max built-ins
For a handful of separate values you don’t need a slice at all. min and max are built into the language and take any number of arguments of the same ordered type:
package main
import "fmt"
func main() {
fmt.Println(min(3, 1, 2))
fmt.Println(max(2.5, 1.0))
fmt.Println(min("pear", "apple"))
}Code language: Go (go)
1
2.5
appleCode language: plaintext (plaintext)
Slices of slices
An element of a slice can be another slice, which gives you a grid. Each row is its own slice, so you create the rows one by one:
package main
import "fmt"
func main() {
grid := make([][]string, 3)
for i := range grid {
grid[i] = make([]string, 3)
for j := range grid[i] {
grid[i][j] = "."
}
}
grid[1][1] = "X"
for _, row := range grid {
fmt.Println(row)
}
}Code language: Go (go)
[. . .]
[. X .]
[. . .]Code language: plaintext (plaintext)
Because every row is separate, rows don’t have to be the same length: [][]int{{1}, {2, 3}, {4, 5, 6}} is perfectly valid.
nil slices and empty slices
The zero value of a slice is nil: no backing array, length 0, capacity 0. An empty slice literal like []int{} also has length 0, but it isn’t nil:
package main
import "fmt"
func main() {
var a []int
b := []int{}
fmt.Println(a == nil, len(a))
fmt.Println(b == nil, len(b))
a = append(a, 1)
fmt.Println(a)
}Code language: Go (go)
true 0
false 0
[1]Code language: plaintext (plaintext)
For nearly everything, the two behave the same: len, range and append all work on a nil slice, which is why var names []string followed by append is the normal way to build a list. So to check whether a slice has anything in it, write len(s) == 0, not s == nil. The one place the difference shows up is when converting to other formats; in Working with JSON you’ll see a nil slice become null and an empty one become [].
If you want to go deeper into how slices work under the hood, the Go blog post Go Slices: usage and internals is a good next read.
Practice
1. Even numbers, largest first. Start with nums := []int{5, 12, 7, 3, 18, 4, 9, 10}. Build a new slice that holds only the even numbers, sort it from largest to smallest, and print it along with how many there are.
Show a solution
package main
import (
"fmt"
"slices"
)
func main() {
nums := []int{5, 12, 7, 3, 18, 4, 9, 10}
var evens []int
for _, n := range nums {
if n%2 == 0 {
evens = append(evens, n)
}
}
slices.Sort(evens)
slices.Reverse(evens)
fmt.Println(evens, len(evens))
}Code language: Go (go)
[18 12 10 4] 4Code language: plaintext (plaintext)2. A safe “last n”. Write a function lastN(s []int, n int) []int that returns the last n elements of s (or all of them if s is shorter). Changing the returned slice, or appending to it, must never change s. Prove it in main.
Show a solution
package main
import (
"fmt"
"slices"
)
func lastN(s []int, n int) []int {
n = min(n, len(s))
return slices.Clone(s[len(s)-n:])
}
func main() {
data := []int{1, 2, 3, 4, 5}
tail := lastN(data, 2)
tail[0] = 40
tail = append(tail, 6)
fmt.Println(data)
fmt.Println(tail)
fmt.Println(lastN(data, 10))
}Code language: Go (go)
[1 2 3 4 5]
[40 5 6]
[1 2 3 4 5]Code language: plaintext (plaintext)Without slices.Clone, tail[0] = 40 would have changed data[3]. Try removing it and see.
Key takeaways
- Arrays have a fixed length that’s part of their type and are copied on assignment. You’ll mostly use slices instead.
- A slice is a pointer, a length and a capacity: a window onto a backing array.
- Always write
s = append(s, …). When capacity runs out,appendmoves the data to a bigger array. - Slicing doesn’t copy. Slices cut from the same slice can overwrite each other’s data; use
slices.Cloneorcopywhen you need independence. - Reach for the
slicespackage (Sort,Contains,Index,Equal, …) and themin/maxbuilt-ins before writing your own loops. - A nil slice works like an empty one. Test for emptiness with
len(s) == 0.