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)

A map finds a value by its key ages["Jafer"] hashes the key to find its slot. A missing key gives the zero value, and the second result says whether it existed. "Vignesh" 26 "Jafer" 28 "Aron" 24 keys values hash age, ok := ages["Nobody"] age = 0 ok = false missing → zero value, never an error ages2 := ages both names, ONE map copy it with maps.Clone
ages["Jafer"] hashes the key to find its slot. A missing key gives the zero value, and the second result says whether it existed.

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 literal map[string]int{"a": 1}.

  • v, ok := m[key] is the comma-ok idiom. ok is true only 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.

Maps are shared, not copied#

references.go

package main

import "fmt"

func main() {
	m1 := map[string]int{"a": 1}
	m2 := m1 // same map, not a copy
	m2["a"] = 100
	fmt.Println(m1, m2)

	var nilMap map[string]int
	fmt.Println(nilMap["x"], len(nilMap)) // reading a nil map is fine
	defer func() { fmt.Println("recovered:", recover()) }()
	nilMap["x"] = 1 // writing panics
}
map[a:100] map[a:100]
0 0
recovered: assignment to entry in nil map

m2 := m1 copies the reference, so both names edit the same map. A nil map can be read but writing to it panics: always create it with make or a literal.

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"] = 1 panics. Use make.

  • Depending on iteration order. It changes from run to run.

  • Reading a missing key and believing it. ages["x"] is 0, not an error. Use comma-ok.

  • Editing a struct value in the map directly. m[k].Field = 1 will 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.