A map stores values under keys, so you can look things up by name instead of by position: a phone number by person, a price by product, a count by word. In this lesson you’ll create maps, read and update them, check whether a key exists, loop over them in a predictable order, and finish with a small word counter.
Creating a map
A map type is written map[KeyType]ValueType. The quickest way to make one is a map literal:
package main
import "fmt"
func main() {
ages := map[string]int{
"Ana": 31,
"Ben": 25,
}
fmt.Println(ages["Ana"])
ages["Cleo"] = 40 // add a new key
ages["Ben"] = 26 // overwrite an existing one
fmt.Println(len(ages))
fmt.Println(ages)
}Code language: Go (go)
31
3
map[Ana:31 Ben:26 Cleo:40]Code language: plaintext (plaintext)
Reading and writing use the same square brackets as slices, but with a key instead of an index. Assigning to a key that doesn’t exist yet adds it; assigning to one that does replaces the value. len tells you how many keys there are. Note the trailing comma after the last entry in the literal: Go requires it when the closing brace is on its own line.
If you want to start with an empty map and fill it later, use make:
package main
import "fmt"
func main() {
prices := make(map[string]float64)
prices["coffee"] = 3.5
prices["bagel"] = 2.25
fmt.Println(prices, len(prices))
}Code language: Go (go)
map[bagel:2.25 coffee:3.5] 2Code language: plaintext (plaintext)
map[string]float64{} (an empty literal) does the same thing. I use make for empty maps and literals when I have starting values, but both are fine.
Keys can be any type that can be compared with ==: strings, numbers, booleans, and some others you’ll meet later. Slices can’t be compared, so they can’t be keys:
package main
import "fmt"
func main() {
m := map[[]int]string{}
fmt.Println(m)
}Code language: Go (go)
./main.go:6:11: invalid map key type []intCode language: plaintext (plaintext)
Values have no such restriction. A value can be anything, including a slice or another map.
Missing keys and the comma-ok idiom
Reading a key that isn’t in the map doesn’t crash and doesn’t return an error. You simply get the zero value of the value type:
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 12, "pears": 0}
fmt.Println(stock["apples"])
fmt.Println(stock["pears"])
fmt.Println(stock["kiwis"])
}Code language: Go (go)
12
0
0Code language: plaintext (plaintext)
That’s convenient (you’ll see why in the word counter), but it raises a question: are we out of pears, or do we not sell kiwis at all? Both print 0. To tell them apart, read the map with two variables. The second one is a bool that says whether the key was present:
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 12, "pears": 0}
n, ok := stock["pears"]
fmt.Println(n, ok)
n, ok = stock["kiwis"]
fmt.Println(n, ok)
if n, ok := stock["apples"]; ok {
fmt.Println("apples in stock:", n)
}
if _, ok := stock["kiwis"]; !ok {
fmt.Println("we don't sell kiwis")
}
}Code language: Go (go)
0 true
0 false
apples in stock: 12
we don't sell kiwisCode language: plaintext (plaintext)
This is called the comma-ok idiom, and the variable is almost always named ok. The if v, ok := m[k]; ok { … } form, with the short statement before the condition (from If and switch), keeps v and ok scoped to the if. You’ll see the same comma-ok pattern again with type assertions and channels.
Deleting keys: delete and clear
The built-in delete removes one key. Deleting a key that isn’t there is fine and does nothing. The built-in clear removes all of them:
package main
import "fmt"
func main() {
ages := map[string]int{"Ana": 31, "Ben": 26, "Cleo": 40}
delete(ages, "Ben")
delete(ages, "Nobody")
fmt.Println(ages, len(ages))
clear(ages)
fmt.Println(ages, len(ages))
}Code language: Go (go)
map[Ana:31 Cleo:40] 2
map[] 0Code language: plaintext (plaintext)
Looping over a map
for key, value := range m visits every entry. There’s one big catch: the order is not defined, and Go deliberately varies it from run to run so nobody comes to depend on it.
package main
import "fmt"
func main() {
capitals := map[string]string{
"France": "Paris",
"Japan": "Tokyo",
"Kenya": "Nairobi",
"Peru": "Lima",
"Iceland": "Reykjavík",
}
for country, city := range capitals {
fmt.Println(country, "→", city)
}
}Code language: Go (go)
Iceland → Reykjavík
France → Paris
Japan → Tokyo
Kenya → Nairobi
Peru → LimaCode language: plaintext (plaintext)
That’s one run on my machine. Run it a few times and you’ll get different orders. (When you print a whole map with fmt.Println, as earlier, fmt sorts the keys for you. That’s a feature of printing, not of the map.)
In sorted key order
When the order matters, get the keys, sort them, and loop over the sorted keys. maps.Keys from the maps package produces the keys, and slices.Sorted collects them into a sorted slice:
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
capitals := map[string]string{
"France": "Paris",
"Japan": "Tokyo",
"Kenya": "Nairobi",
"Peru": "Lima",
"Iceland": "Reykjavík",
}
for _, country := range slices.Sorted(maps.Keys(capitals)) {
fmt.Println(country, "→", capitals[country])
}
}Code language: Go (go)
France → Paris
Iceland → Reykjavík
Japan → Tokyo
Kenya → Nairobi
Peru → LimaCode language: plaintext (plaintext)
Same result every time. maps.Keys doesn’t return a slice but an iterator, something you can range over that hands out one key at a time. You don’t need to know more than that here: slices.Sorted takes an iterator and gives you back a sorted slice. There’s a matching maps.Values if you only care about the values.
Maps of slices
A map value can be a slice, which is a natural way to group things. Thanks to zero values, you don’t need to create the slice before appending: a missing key gives you a nil slice, and append works on nil slices (see Arrays and slices).
package main
import "fmt"
func main() {
byTeam := make(map[string][]string)
byTeam["red"] = append(byTeam["red"], "Ana")
byTeam["blue"] = append(byTeam["blue"], "Ben")
byTeam["red"] = append(byTeam["red"], "Cleo")
fmt.Println(byTeam["red"], len(byTeam["red"]))
fmt.Println(byTeam["blue"])
fmt.Println(byTeam["green"] == nil)
}Code language: Go (go)
[Ana Cleo] 2
[Ben]
trueCode language: plaintext (plaintext)
Maps of structs
You’ll often store a group of related values per key. In Go that group is a struct, the topic of the next lesson. Here’s a preview with one gotcha worth knowing now: you can read a field of a struct stored in a map, but you can’t assign to it directly.
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
places := map[string]Point{"home": {X: 1, Y: 2}}
places["home"].X = 10
fmt.Println(places)
}Code language: Go (go)
./main.go:11:2: cannot assign to struct field places["home"].X in mapCode language: plaintext (plaintext)
Reading places["home"] gives you a copy of the struct, so changing that copy would have no effect, and the compiler refuses. Take the value out, change it, and put it back:
package main
import "fmt"
type Point struct {
X, Y int
}
func main() {
places := map[string]Point{"home": {X: 1, Y: 2}}
p := places["home"]
p.X = 10
places["home"] = p
fmt.Println(places["home"])
}Code language: Go (go)
{10 2}Code language: plaintext (plaintext)
Maps are references
A map variable doesn’t hold the entries itself; it refers to a data structure elsewhere in memory. Copying the variable or passing it to a function copies only that reference, so both names see the same map, and a function can add or change entries for its caller:
package main
import (
"fmt"
"maps"
)
func addBonus(scores map[string]int) {
for name := range scores {
scores[name] += 10
}
}
func main() {
scores := map[string]int{"Ana": 80, "Ben": 72}
alias := scores
alias["Cleo"] = 90
fmt.Println(scores)
addBonus(scores)
fmt.Println(scores)
backup := maps.Clone(scores)
backup["Ana"] = 0
fmt.Println(scores["Ana"], backup["Ana"])
}Code language: Go (go)
map[Ana:80 Ben:72 Cleo:90]
map[Ana:90 Ben:82 Cleo:100]
90 0Code language: plaintext (plaintext)
When you want an independent copy, use maps.Clone. Changing the clone leaves the original alone.
Writing to a nil map panics
The zero value of a map type is nil. A nil map behaves like an empty map when you read from it (len is 0, every lookup returns the zero value, range does nothing), but writing to it crashes the program:
package main
import "fmt"
func main() {
var counts map[string]int
fmt.Println(counts["x"], len(counts))
counts["x"] = 1
fmt.Println("never printed")
}Code language: Go (go)
0 0
panic: assignment to entry in nil map
goroutine 1 [running]:
main.main()
/home/you/maps/main.go:9 +0xa4
exit status 2Code language: plaintext (plaintext)
That’s what go run prints; the path and the hex number on the last lines will be different for you. The first line of a panic is the one to read: it tells you what went wrong, and the main.go:9 below it tells you where.
var m map[string]int declares a map variable but creates no map. Before you write to it, give it a map with make or a literal: counts := make(map[string]int).
Example: counting words
Here’s everything working together. The program splits a text into words, counts each one, and prints the counts in alphabetical order:
package main
import (
"fmt"
"maps"
"slices"
"strings"
)
func main() {
text := "The cat sat. The cat ran! A dog sat, and the dog barked."
counts := make(map[string]int)
for _, word := range strings.Fields(text) {
word = strings.ToLower(strings.Trim(word, ".,!?"))
counts[word]++
}
for _, word := range slices.Sorted(maps.Keys(counts)) {
fmt.Printf("%-7s %d\n", word, counts[word])
}
}Code language: Go (go)
a 1
and 1
barked 1
cat 2
dog 2
ran 1
sat 2
the 3Code language: plaintext (plaintext)
The line that does the real work is counts[word]++. The first time a word shows up, counts[word] reads as 0 (the zero value), so it becomes 1. No “is this key already there?” check needed. strings.Fields splits on whitespace, strings.Trim strips punctuation from both ends, and strings.ToLower makes “The” and “the” the same word (all from Strings). %-7s pads the word to 7 characters so the numbers line up.
Practice
1. Letter count. Count how often each letter appears in "mississippi" and print the letters in alphabetical order with their counts. Hint: ranging over a string gives you runes, and string(r) turns a rune into a string you can use as a key.
Show a solution
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
counts := make(map[string]int)
for _, r := range "mississippi" {
counts[string(r)]++
}
for _, letter := range slices.Sorted(maps.Keys(counts)) {
fmt.Println(letter, counts[letter])
}
}Code language: Go (go)
i 4
m 1
p 2
s 4Code language: plaintext (plaintext)2. Group by length. Given words := []string{"go", "map", "slice", "key", "range", "nil", "if"}, build a map[int][]string from word length to the words of that length, then print the groups from shortest to longest.
Show a solution
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
words := []string{"go", "map", "slice", "key", "range", "nil", "if"}
byLen := make(map[int][]string)
for _, w := range words {
byLen[len(w)] = append(byLen[len(w)], w)
}
for _, n := range slices.Sorted(maps.Keys(byLen)) {
fmt.Println(n, byLen[n])
}
}Code language: Go (go)
2 [go if]
3 [map key nil]
5 [slice range]Code language: plaintext (plaintext)Key takeaways
- Create maps with a literal or
make(map[K]V); keys must be comparable with==. - A missing key reads as the zero value. Use
v, ok := m[k]when you need to know whether the key exists. deleteremoves one key,clearremoves all of them.- Iteration order is random. For a stable order, range over
slices.Sorted(maps.Keys(m)). - Maps are references: copies and function arguments share the same entries. Use
maps.Clonefor a real copy. - A nil map can be read but not written. Make the map before you store anything in it.