Session 3 · Arrays and slices#
Watch the class#
The big idea#
You often need an ordered list of things. Go gives you two: the array, which has a fixed size that is part of its type, and the slice, a flexible window onto an array that can grow. In real Go code you will use slices about 99 % of the time. You still need to understand arrays, because slices are built on top of them.
Everyday example: an egg carton versus a window. The carton holds exactly 12 eggs; that is an array. A slice is like a window cut into the carton wall: it shows eggs 3 to 6 and nothing else, and if someone moves an egg you see the change too, because the window and the carton are the same eggs.
In plain words
[3]int is a fixed row of three ints. []int is a slice: a length, a capacity and a pointer to
an array underneath. append adds to a slice and may move it to a bigger array.
Reach for it when:
you have a list whose size changes: use a slice
you have a fixed, tiny shape such as
[2]intcoordinates: an array is fineyou want to share or cut up data without copying: slice it, but remember the sharing
Arrays#
arrays.go
package main
import "fmt"
func main() {
var grades [5]int
grades[0] = 95
colors := [...]string{"Red", "Green", "Blue"}
fmt.Println(grades, len(grades))
fmt.Println(colors, len(colors))
// arrays are copied when assigned
a := [3]int{1, 2, 3}
b := a
b[0] = 99
fmt.Println(a, b)
for index, value := range [3]string{"Go", "Python", "Rust"} {
fmt.Printf("Index %d holds %s\n", index, value)
}
}
[95 0 0 0 0] 5
[Red Green Blue] 3
[1 2 3] [99 2 3]
Index 0 holds Go
Index 1 holds Python
Index 2 holds Rust
The size is part of the type, so [3]int and [4]int are different types. Assigning an array
copies it, which is why b[0] = 99 left a alone. [...]string{...} lets the compiler count.
Slices#
slices.go
package main
import "fmt"
func main() {
s := []int{1, 2, 3}
s = append(s, 4, 5)
fmt.Println(s, len(s), cap(s))
t := make([]int, 2, 10) // length 2, room for 10
fmt.Println(t, len(t), cap(t))
// slicing: [start:end), end not included
fmt.Println(s[1:3], s[:2], s[3:])
// a slice is a window onto the same array
window := s[1:3]
window[0] = 99
fmt.Println(s, window)
// copy makes an independent slice
clone := make([]int, len(s))
copy(clone, s)
clone[0] = -1
fmt.Println(s, clone)
// removing index 1 by stitching two pieces
s = append(s[:1], s[2:]...)
fmt.Println(s)
}
[1 2 3 4 5] 5 6
[0 0] 2 10
[2 3] [1 2] [4 5]
[1 99 3 4 5] [99 3]
[1 99 3 4 5] [-1 99 3 4 5]
[1 3 4 5]
lenis how many elements you can see;capis how many fit before Go must find a bigger array.s[1:3]includes index 1 and stops before 3.A sub-slice shares the same array. Writing through
windowchangeds.copy(dst, src)makes an independent copy.append(s[:1], s[2:]...)deletes one element.make([]int, 2, 10)makes a slice with length 2 and room for 10.
How a slice grows#
growth.go prints every time the capacity changes while we append 20 numbers.
package main
import "fmt"
func main() {
var s []int
last := -1
for i := 0; i < 20; i++ {
s = append(s, i)
if cap(s) != last {
fmt.Printf("len=%2d cap=%2d\n", len(s), cap(s))
last = cap(s)
}
}
}
len= 1 cap= 4
len= 5 cap= 8
len= 9 cap=16
len=17 cap=32
Go doubles the capacity as the slice grows, so adding items stays cheap. The exact numbers depend
on the Go version, so never rely on them; rely on len.
Common mistakes#
Always using the return value of
append. Writingappend(s, 1)on its own line does nothing useful. Writes = append(s, 1).Surprise sharing. Two slices of one array change together. Use
copywhen you need your own data.Index out of range.
s[5]on a 3-element slice panics. Checklen(s)first.Using an array when you wanted a slice. Arrays are copied on every function call, which is slow for big ones and does not let the function change your data.
for i, v := range sand changingv.vis a copy of the element. Changes[i].
Hands-on exercises#
Exercise 1 · Reverse. Write reverse(s []int) that reverses a slice in place.
Solution
package main
import "fmt"
func reverse(s []int) {
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
}
func main() {
nums := []int{1, 2, 3, 4, 5}
reverse(nums)
fmt.Println(nums)
}
[5 4 3 2 1]
Because a slice shares its array, the function changes the caller’s data without returning anything.
Exercise 2 · Filter. Write evens(nums []int) []int that returns only the even numbers.
Solution
package main
import "fmt"
func evens(nums []int) []int {
var out []int
for _, n := range nums {
if n%2 == 0 {
out = append(out, n)
}
}
return out
}
func main() {
fmt.Println(evens([]int{1, 2, 3, 4, 5, 6}))
fmt.Println(evens([]int{1, 3}) == nil)
}
[2 4 6]
true
A nil slice is a perfectly good empty slice: you can append to it and loop over it.
Exercise 3 · Break the sharing. Make b a slice with the same numbers as a but changing b[0]
must not change a.
Solution
package main
import "fmt"
func main() {
a := []int{1, 2, 3}
b := make([]int, len(a))
copy(b, a)
b[0] = 100
fmt.Println(a, b)
}
[1 2 3] [100 2 3]