Session 4 · Maps#
Watch the class#
The big idea#
A map stores pairs: look something up by a key and get its value. It answers “what is the age of Jafer?” instantly, however many people are stored, without scanning a list.
Everyday example: a phone’s contact list. You type a name and get a number. You do not read every contact from the top.
In plain words
map[string]int maps text keys to int values. Reading a key that is not there gives the zero value
(so you cannot tell “missing” from “zero”), unless you ask for the second result: v, ok := m[k].
Reach for it when:
you look things up by name or id (users by id, counts per word)
you want to count or group things
you need “have I seen this before?” (a map used as a set)
The basics#
maps.go
package main
import (
"fmt"
"sort"
)
func main() {
ages := make(map[string]int)
ages["Vignesh"] = 26
ages["Jafer"] = 28
ages["Aron"] = 24
fmt.Println(ages["Jafer"], len(ages))
fmt.Println(ages["Nobody"]) // missing key: zero value
value, exists := ages["Nobody"]
fmt.Println(value, exists)
if age, ok := ages["Aron"]; ok {
fmt.Println("Aron is", age)
}
delete(ages, "Vignesh")
// map order is random, so sort the keys when order matters
keys := make([]string, 0, len(ages))
for k := range ages {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Println(k, ages[k])
}
}
28 3
0
0 false
Aron is 24
Aron 24
Jafer 28
Make one with
make(map[K]V)or a literalmap[string]int{"a": 1}.v, ok := m[key]is the comma-ok idiom.okistrueonly if the key exists.delete(m, key)removes a key and does nothing if it is absent.Iteration order is random on purpose. To print in order, collect and sort the keys.
Keys must be comparable: strings, numbers, bools, arrays and structs of those work; slices do not.
Worked example: expense tracker#
The original course builds an expense tracker with a map of ids to expenses. With a struct as the
value it becomes safer than a map of interface{}.
package main
import (
"fmt"
"sort"
)
type Expense struct {
Description string
Amount float64
Category string
}
var expenses = map[int]Expense{}
func add(id int, e Expense) { expenses[id] = e }
func update(id int, amount float64) {
e, ok := expenses[id]
if !ok {
fmt.Println("Expense not found:", id)
return
}
e.Amount = amount // e is a copy, so store it back
expenses[id] = e
}
func main() {
add(1, Expense{"Tea", 20, "Food"})
add(2, Expense{"Bus", 35.5, "Travel"})
update(1, 25)
update(9, 1)
ids := make([]int, 0)
for id := range expenses {
ids = append(ids, id)
}
sort.Ints(ids)
total := 0.0
byCategory := map[string]float64{}
for _, id := range ids {
e := expenses[id]
fmt.Printf("%d %-4s %6.2f %s\n", id, e.Description, e.Amount, e.Category)
total += e.Amount
byCategory[e.Category] += e.Amount
}
fmt.Println("total:", total, byCategory)
}
Expense not found: 9
1 Tea 25.00 Food
2 Bus 35.50 Travel
total: 60.5 map[Food:25 Travel:35.5]
Notice update: expenses[id] returns a copy of the struct, so changing e.Amount is not
enough. You must store e back with expenses[id] = e.
Common mistakes#
Writing to a nil map.
var m map[string]int; m["a"] = 1panics. Usemake.Depending on iteration order. It changes from run to run.
Reading a missing key and believing it.
ages["x"]is0, not an error. Use comma-ok.Editing a struct value in the map directly.
m[k].Field = 1will not compile; copy, change, store back (or store pointers).Sharing a map between goroutines without a lock. Maps are not safe for concurrent writes (Session 16).
Hands-on exercises#
Exercise 1 · Word counter. Count how many times each word appears in "to be or not to be" and
print them sorted by word.
Solution
package main
import (
"fmt"
"sort"
"strings"
)
func main() {
counts := map[string]int{}
for _, w := range strings.Fields("to be or not to be") {
counts[w]++
}
words := make([]string, 0, len(counts))
for w := range counts {
words = append(words, w)
}
sort.Strings(words)
for _, w := range words {
fmt.Println(w, counts[w])
}
}
be 2
not 1
or 1
to 2
counts[w]++ works even for a new word, because the missing key reads as 0.
Exercise 2 · Set of unique numbers. Remove duplicates from [3 1 3 2 1] keeping first-seen order.
Solution
package main
import "fmt"
func main() {
input := []int{3, 1, 3, 2, 1}
seen := map[int]bool{}
var unique []int
for _, n := range input {
if !seen[n] {
seen[n] = true
unique = append(unique, n)
}
}
fmt.Println(unique)
}
[3 1 2]
map[T]bool is Go’s everyday set.
Exercise 3 · Group by length. Group ["go", "is", "fun", "and", "fast"] into a
map[int][]string by word length.
Solution
package main
import "fmt"
func main() {
groups := map[int][]string{}
for _, w := range []string{"go", "is", "fun", "and", "fast"} {
groups[len(w)] = append(groups[len(w)], w)
}
fmt.Println(groups) // fmt prints maps with sorted keys
}
map[2:[go is] 3:[fun and] 4:[fast]]
append on a missing key starts from a nil slice, which is fine.