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"])    // 95

One 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)) // 1

There 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]) // 95

Adding 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)) // 3

After 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"]) // 0

This 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 false

Use 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]) // sword

map[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])) // 3

This 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.