All parts · this is part 4
- 1.Introduction to Go
- 2.Constants, Variables and Basic Data Types
- 3.Functions and Control Structures
- 4.Arrays, Slices, Maps and Loops
- 5.Strings, Runes and Bytes
- 6.Structs and Interfaces
- 7.Pointers
- 8.Goroutines
- 9.Channels
- 10.Generics
- 11.Building an API
Extras
Arrays
An array in Go is:
- A fixed length. Once you make it, it can't grow or shrink.
- All the same type.
- Indexable, starting at 0.
- Stored in one continuous chunk of memory.
Let's go through those with some code:
package main
import "fmt"
func main() {
var intArr [3]int32 // three int32s, all starting at 0
fmt.Println(intArr) // [0 0 0]
intArr[1] = 123
fmt.Println(intArr[0]) // 0
fmt.Println(intArr[1:3]) // [123 0]
// Each element sits right after the last one, 4 bytes apart.
fmt.Println(&intArr[0])
fmt.Println(&intArr[1])
fmt.Println(&intArr[2])
// Other ways to make one:
var a [3]int32 = [3]int32{1, 2, 3}
b := [3]int32{1, 2, 3}
c := [...]int32{1, 2, 3} // the compiler counts them for you
fmt.Println(a, b, c)
}The [3] is the length, and int32 is the type of everything in it. Since we didn't set any values, each element gets the zero value for int32, which is 0.
Now about that memory part. An int32 is 4 bytes, so an array of three of them gets 12 bytes in a row. If you print the addresses with & (more on that in part 7), you'll see they go up by 4 each time. That's nice for the computer: it only has to remember where the first one is, and it can find the rest by counting.
One thing that surprises people: arrays are values. Assign one to another variable or pass it into a function and the whole thing gets copied.
package main
import "fmt"
func main() {
original := [3]int{1, 2, 3}
copied := original // the whole array gets copied
copied[0] = 100
fmt.Println(original) // [1 2 3]
fmt.Println(copied) // [100 2 3]
fmt.Println(original == [3]int{1, 2, 3}) // true: arrays can be compared with ==
}Practice
- Practice with AI
PracticeIs it a copy?
A function sets the first element of a [3]int to 99, and main prints the array after calling it. Guess what it prints, then run it. Did it change? Why?
- Practice with AI
Warm-upAdd them up
Read five ints into an array, then loop through it and print the total.
Slices
According to the Go docs, slices are just wrappers around arrays. So under the hood there's still an array, but the slice gives you some extra stuff on top, the big one being that it can grow. To make one, just leave the length out of the brackets:
package main
import "fmt"
func main() {
intSlice := []int32{4, 5, 6} // no length in the brackets: it's a slice
fmt.Printf("length %v, capacity %v\n", len(intSlice), cap(intSlice)) // 3, 3
intSlice = append(intSlice, 7)
fmt.Printf("length %v, capacity %v\n", len(intSlice), cap(intSlice)) // 4, 6
// fmt.Println(intSlice[4]) // panic: index out of range, even though cap is 6
more := []int32{8, 9}
intSlice = append(intSlice, more...) // ... spreads a slice into append
fmt.Println(intSlice) // [4 5 6 7 8 9]
withCap := make([]int32, 3, 8) // length 3, room for 8
fmt.Println(withCap, len(withCap), cap(withCap)) // [0 0 0] 3 8
}So what's going on with append? The slice starts with an array that can hold exactly 3 values. When we append a 4th, Go checks if there's room. There isn't, so it makes a brand new, bigger array, copies the values over, and adds the new one. That's why you always write s = append(s, x): you might be getting back a slice pointing at a totally different array.
That's also where length and capacity come from. Length is how many values the slice has. Capacity is how many fit in the array underneath before it needs a new one. After that append we've got length 4, capacity 6. Those extra 2 spots exist, but you can't touch them, so intSlice[4] is still out of range.
You can also make a slice with make, giving it a length and, if you want, a capacity. If you have a rough idea of how big it's going to get, setting the capacity up front saves Go from making new arrays over and over. We'll time that at the end of this part.
Practice
- Practice with AI
PracticeEvens only
Write evens(nums []int) []int that returns a new slice with just the even numbers.
- Practice with AI
Warm-upWatch it grow
Start with an empty slice and append 20 numbers one at a time, printing len and cap after each. When does the capacity jump?
- Practice with AI
StretchReverse it
Write reverse(s []int) that flips a slice in place, without making a new one.
Maps
A map is a bunch of key value pairs where you look up a value by its key. You can make one with make, or fill it in right away:
package main
import "fmt"
func main() {
var empty map[string]uint8 = make(map[string]uint8)
fmt.Println(empty) // map[]
ages := map[string]uint8{"Adam": 23, "Sarah": 45}
fmt.Println(ages["Adam"]) // 23
fmt.Println(ages["Jason"]) // 0: not there, so you get the zero value
age, ok := ages["Jason"]
if ok {
fmt.Printf("The age is %v\n", age)
} else {
fmt.Println("Invalid name")
}
ages["Jason"] = 31
delete(ages, "Adam")
fmt.Println(len(ages)) // 2
}map[string]uint8 means the keys are strings and the values are uint8s. Now, you have to be careful here. If you look up a key that isn't in the map, Go doesn't give you an error. It gives you the zero value for the type. So ages["Jason"] is 0, and you can't tell if Jason is a newborn or just isn't in there.
Luckily, maps give back an optional second value, a bool that's true if the key was actually there. People usually call it ok. To remove something, use delete. It changes the map in place, so it doesn't return anything.
One more thing to watch out for. If you declare a map but never make it, it's nil, and writing to it crashes:
package main
import "fmt"
func main() {
var scores map[string]int // declared, but never made: it's nil
fmt.Println(scores["x"]) // 0: reading a nil map is fine
// scores["x"] = 1 // panic: assignment to entry in nil map
scores = map[string]int{} // now it's a real (empty) map
scores["x"] = 1
fmt.Println(scores)
}Practice
- Practice with AI
Warm-upCount the words
Write wordCount(words []string) map[string]int that counts how many times each word shows up.
- Practice with AI
PracticeIs it there or is it zero?
With a map of item names to stock counts where one item has 0 in stock, write lookup(stock map[string]int, item string) string that tells apart "out of stock" and "we don't sell that".
- Practice with AI
StretchGroup by length
Write groupByLength(words []string) map[int][]string that groups words by how many letters they have.
Loops
Go only has one loop keyword, for, but you can write it a bunch of different ways. To go through a map, array or slice, use range:
package main
import "fmt"
func main() {
ages := map[string]uint8{"Adam": 23, "Sarah": 45}
for name, age := range ages {
fmt.Printf("Name: %v, Age: %v\n", name, age) // order changes run to run
}
intArr := []int32{1, 2, 3}
for i, v := range intArr {
fmt.Printf("Index: %v, Value: %v\n", i, v)
}
for _, v := range intArr { // don't need the index? use _
fmt.Println(v)
}
i := 0
for i < 3 { // Go's "while" loop
fmt.Println(i)
i++
}
for { // loops forever until something breaks out
if i >= 5 {
break
}
i++
}
for j := 0; j < 10; j++ { // start; keep going while; after each loop
if j%2 == 0 {
continue // skip the rest of this loop and go to the next one
}
fmt.Println(j)
}
for k := range 3 { // Go 1.22+: 0, 1, 2
fmt.Println(k)
}
}So the different flavors are:
| You write | It does |
|---|---|
| for i, v := range s | Goes through a slice, array or map (or string, part 5, or channel, part 9) |
| for i < 10 | Keeps going while the condition is true. This is Go's while loop |
| for { } | Goes forever, until you break out |
| for i := 0; i < 10; i++ | The classic: start, keep going while, then what to do after each loop |
| for i := range 10 | Counts 0 to 9 (Go 1.22 and up) |
break gets you out of the loop completely, and continue skips to the next time around. And i++ is just short for i = i + 1.
Notice the map loop: run it a few times and the order changes. Go doesn't keep maps in any order, and it actually shuffles them on purpose so you don't start relying on one. If you need them in order, sort the keys:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
ages := map[string]int{"Sarah": 45, "Adam": 23, "Jason": 31}
for _, name := range slices.Sorted(maps.Keys(ages)) {
fmt.Println(name, ages[name]) // Adam, Jason, Sarah, every time
}
}Practice
- Practice with AI
Warm-upCount to 5, four ways
Read a number n, then print 1 to n using a three-part for loop, a while-style loop, an infinite loop with break, and range.
- Practice with AI
PracticeFind the first one
Write indexOf(s []string, target string) int that returns the index of target, or -1. Stop looping as soon as you find it.
- Practice with AI
StretchPrint a map in order
Write printByScore(scores map[string]int) that prints the names and scores sorted by score, highest first, with ties in alphabetical order. Hint: sort.Slice or slices.SortFunc will help.
Preallocating capacity
As promised, let's see how much setting the capacity ahead of time actually helps. We'll append a million values to two slices: one that starts empty with no capacity, and one that starts empty but with room for a million.
package main
import (
"fmt"
"time"
)
func timeLoop(slice []int, n int) time.Duration {
start := time.Now()
for len(slice) < n {
slice = append(slice, 1)
}
return time.Since(start)
}
func main() {
n := 1_000_000
empty := []int{}
preallocated := make([]int, 0, n)
fmt.Println("no capacity: ", timeLoop(empty, n))
fmt.Println("preallocated: ", timeLoop(preallocated, n))
}In the video the one with no capacity took about three times as long, and depending on your machine it could be even worse. Every time it runs out of room, Go has to make a bigger array and copy everything over, and that adds up.
Practice
- Practice with AI
PracticeMake it once
Fill in squares(n int) []int, which gives back the squares of 1 to n, so it makes the slice once with make and the right capacity, and never has to grow. The tests check that the result's capacity is exactly n.