23. Maps
A map connects keys to values. A slice finds a value by its position (0, 1, 2). A map finds it by a key, the way you look a word up in a dictionary.
Creating a map
A map literal is the type, followed by key: value pairs
in curly braces:
stats := map[string]int{
"level": 7,
"hp": 95,
"coins": 12,
}Read map[string]int as "a map from strings to ints". The
keys are all strings, and the values are all ints. On several lines,
every pair needs a comma after it, even the last one. Without that last
comma, Go stops:
syntax error: unexpected newline in composite literal; possibly missing comma or }
Read a value with square brackets:
stats := map[string]int{
"level": 7,
"hp": 95,
"coins": 12,
}
fmt.Println(stats["level"]) // 7
fmt.Println(stats["hp"]) // 95One map has one value type. This is a real limit in Go, and it shapes the rest of this chapter. A name is a string and a level is an int, so they do not fit in the same map:
player := map[string]int{
"name": "Keiko",
"level": 7,
}
fmt.Println(player)cannot use "Keiko" (untyped string constant) as int value in map literal
For now, keep values of different types in different variables or different maps. Chapter 26 introduces structs, which hold values of different types together.
To start with an empty map and fill it later, use make,
which you met in Chapter 22. len gives the number of
keys:
stats := make(map[string]int)
stats["level"] = 1
fmt.Println(len(stats)) // 1There is no dot notation for maps. stats.level does not
compile, and you always write stats["level"]:
stats.level undefined (type map[string]int has no field or method level)
When the key is held in a variable, that works too:
stats := map[string]int{"level": 7, "hp": 95}
field := "hp"
fmt.Println(stats[field]) // 95Adding and changing values
Assigning to a new key creates it. Assigning to an existing key
replaces it. delete removes one:
stats := map[string]int{"level": 7, "hp": 95, "coins": 12}
stats["mana"] = 30 // new key
stats["hp"] = 100 // replace existing
delete(stats, "coins")
fmt.Println(len(stats)) // 3After those three lines, stats holds
"level", "hp" and "mana".
Deleting a key that is not there does nothing. These examples are in
exercises/23/01-map.go.
Looping with range
range gives you every key and value in the map:
for key, value := range stats {
fmt.Println(key, value)
}A map has no order. Go visits the keys in a random
order, and the order changes from one loop to the next.
exercises/23/04-random-order.go loops over the same map
three times, then prints the sorted names. The first three lines of the
following output come from one real run, and yours will differ:
Ada Ben Cara Dan Eve
Eve Ada Ben Cara Dan
Dan Eve Ada Ben Cara
[Ada Ben Cara Dan Eve]
Never write a program whose output depends on this order.
When you need a fixed order, collect the keys into a slice, sort it,
and loop over the slice. slices.Sort, from the
slices package, sorts a slice in place. Chapter 24 covers
that package. Add "slices" to your imports:
stats := map[string]int{"level": 7, "hp": 100, "mana": 30}
keys := []string{}
for key := range stats {
keys = append(keys, key)
}
slices.Sort(keys)
for _, key := range keys {
fmt.Println(key, stats[key])
}Output, which is the same on every run:
hp 100
level 7
mana 30
For a quick look at a whole map, fmt.Println prints it
with its keys sorted. That only applies to printing, not to
range:
stats := map[string]int{"level": 7, "hp": 100, "mana": 30}
fmt.Println(stats) // map[hp:100 level:7 mana:30]It is safe to delete the key you are on during a
range loop.
Checking if a key exists
Asking for a missing key does not crash. It gives the zero
value of the value type, which is 0 for an int and
"" for a string. So a missing key can look just like a real
0:
stats := map[string]int{"level": 7, "hp": 0}
fmt.Println(stats["hp"]) // 0
fmt.Println(stats["email"]) // 0This is the same problem as bad text and strconv.Atoi in
Chapter 21, and the answer has the same shape. Ask for two values. The
second is a bool, usually called ok, that says whether the
key is there:
stats := map[string]int{"level": 7, "hp": 0}
hp, ok := stats["hp"]
fmt.Println(hp, ok) // 0 true
mana, ok := stats["mana"]
fmt.Println(mana, ok) // 0 falseUse it with if:
stats := map[string]int{"level": 7, "hp": 0}
coins, ok := stats["coins"]
if ok {
fmt.Println("Coins:", coins)
} else {
fmt.Println("No coins on file.")
}Output:
No coins on file.
A map declared with var and no value, as in
var stats map[string]int, is not ready to use. Reading from
it gives zero values, but assigning to it crashes with
panic: assignment to entry in nil map. Make maps with a
literal or make, and this will not happen.
Maps are shared when you pass them to a function, as slices are. A function that sets a key sets it for the caller too, with no need to return anything:
func addCoins(stats map[string]int, amount int) {
stats["coins"] += amount
}
func main() {
stats := map[string]int{"coins": 5}
addCoins(stats, 10)
fmt.Println(stats["coins"]) // 15
}Nested maps
A map's value can be another map, or a slice. The type says so:
positions := map[string]map[string]int{
"Keiko": {"x": 0, "y": 5, "z": -3},
}
inventories := map[string][]string{
"Keiko": {"sword", "potion", "map"},
}
fmt.Println(positions["Keiko"]["x"]) // 0
fmt.Println(inventories["Keiko"][0]) // swordmap[string]map[string]int reads as "a map from strings
to maps from strings to ints". Inside the literal, Go lets you leave out
the inner type, so {"x": 0, ...} is enough.
To grow a slice held in a map, assign the result of
append back to that key:
inventories["Keiko"] = append(inventories["Keiko"], "rope")These examples are in exercises/23/02-nested.go.
Maps and slices together
A quest has a name, a reward and a list of steps. Those are three different types, and one map cannot hold them. A way that works at this stage is two maps that use the same keys, one map for each kind of information:
questSteps := map[string][]string{
"Healer's Errand": {
"Speak to the elder",
"Collect 5 herbs",
"Return to camp",
},
}
questRewards := map[string]int{
"Healer's Errand": 50,
}
name := "Healer's Errand"
fmt.Println(questRewards[name]) // 50
fmt.Println(questSteps[name][0]) // Speak to the elder
fmt.Println(len(questSteps[name])) // 3This is exercises/23/03-mixed.go. The Part 5
mini-project builds its whole game world this way. It works, but nothing
keeps the two maps in step. If you add a quest to one and forget the
other, Go will not tell you. Chapter 26 fixes that.
Homework
Homework starters are in exercises/23/homework/. Each
one runs as it is and prints a placeholder line.
Problem 1 — Player record
Open exercises/23/homework/01-player-record.go. Build a
map[string]string called player with at least
these four keys: "name", "class",
"level", "alive". All the values are text,
such as "7" and "true", because one map has
one value type. Then print every key and value, one per line, in sorted
order of the keys.
Problem 2 — Inventory weights
Open exercises/23/homework/02-inventory-weights.go. A
map connects item names to weights. Compute and print the total weight
with two decimals.
Problem 3 — Safe lookup
Open exercises/23/homework/03-safe-lookup.go. Write a
function
lookup(player map[string]string, key string) string that
returns the value if the key exists, else the string
"(not found)". Test it with three calls: one that hits, one
that misses, and one where the key is held in a variable.
Challenge — Word frequency
Open exercises/23/homework/04-word-frequency.go. Given a
slice of words (some repeated), build a map from each unique word to its
count. Print one pair per line, in sorted order of the words.
Stuck or finished? Open the homework solutions page.