Maps
A map is Go's built-in hash table: an unordered collection of key-value pairs where each key is unique. Other languages call the same thing a dictionary, a hash, or an associative array. Together with the slice, it is the collection type you will reach for most often in real Go code.
Declaring a map
There are two common ways to create one. The literal form lists the pairs inline:
ages := map[string]int{
"alice": 30,
"bob": 25,
}
fmt.Println(ages) // map[alice:30 bob:25]
The type map[string]int reads as "map from string keys to int values". Both the key type and the value type go into the declaration, so the compiler knows exactly what you are allowed to read and write.
The other way is make, which creates an empty map ready to be filled later:
scores := make(map[string]int)
scores["alice"] = 100
scores["bob"] = 95
fmt.Println(scores) // map[alice:100 bob:95]
Unlike slices, there is no length argument; maps grow as you add to them. make(map[K]V, n) accepts an optional size hint that lets the runtime pre-size the bucket table for roughly n entries. It is advisory rather than a hard reservation, but on normal workloads it saves the rehashes the map would otherwise do as it grows, so pass it when you already know the rough target size.
What can be a key
The map's key type has to be comparable: something you can test with ==. That covers the types you have met so far (string, all the numeric types, bool) and, looking ahead, structs whose fields are all themselves comparable.
What is not comparable, and therefore cannot be a key: slices, maps, and functions. Try to use one and the compiler stops you:
bad := map[[]string]int{}
// compile error: invalid map key type []string
The reason is the one you met in the previous lesson: == on slices would need a decision between header-identity and value-equality, so Go refuses to pick, slices are not comparable, and therefore cannot be keys.
When you genuinely need a composite key, the workaround is to build a comparable representation. A common pattern is to join values into a string ("alice|1234"), or, once you meet structs in the next chapter, to use a struct of comparable fields as the key directly.
Reading and writing
Assignment and lookup share the same square-bracket syntax:
ages := map[string]int{"alice": 30}
ages["bob"] = 25
fmt.Println(ages["alice"]) // 30
fmt.Println(ages["bob"]) // 25
Writing a key that is not yet in the map adds it. Writing one that is already there replaces the old value.
Missing keys return the zero value
Here is the surprising-but-deliberate rule, worth internalising before you build anything non-trivial. Reading a key that does not exist returns the zero value of the map's value type. Not an error, not a panic:
ages := map[string]int{"alice": 30}
fmt.Println(ages["alice"]) // 30
fmt.Println(ages["nobody"]) // 0 (zero value of int, not an error)
ages["nobody"] is indistinguishable by value from ages containing "nobody": 0. That is convenient for counting patterns: counts[word]++ just works on an empty map because the initial read returns 0 and ++ bumps it to 1, even though word had never been seen. But it means you cannot use the result alone to tell "missing" from "present-with-zero-value". The next lesson covers the comma-ok form that makes the distinction explicit.
The nil-map trap
The zero value of a map type is nil, the same way it is for a slice. You can read from a nil map (every read returns the zero value), but writing to a nil map is a runtime panic.
A freshly declared map variable like var m map[string]int is nil. Reading from it is safe (every lookup returns the zero value), but writing to it crashes the program. Try it in the editor on this site:
var m map[string]int
fmt.Println(m["x"]) // 0, fine
m["a"] = 1 // runtime panic: assignment to entry in nil map
Recover by changing the declaration to m := map[string]int{} or m := make(map[string]int). The bug var m map[string]int followed by m[key] = value is the most common first-time Go mistake with maps, usually showing up where a struct field is a map and its constructor forgot to initialise it.
Maps are reference types
Like slices, maps are reference types. Assigning a map to another variable copies a small header; both variables end up pointing at the same underlying hash table:
a := map[string]int{"x": 1}
b := a
b["x"] = 99
fmt.Println(a["x"]) // 99
Pass a map to a function and mutations inside the function are visible to the caller, without any extra ceremony:
func add(m map[string]int, key string, value int) {
m[key] = value
}
func main() {
counts := map[string]int{"x": 1}
add(counts, "y", 2)
fmt.Println(counts) // map[x:1 y:2]
}
The map received by add is the same hash table counts already held. Writing m[key] = value inside the function lands in the caller's table with no return value, no pointer parameter, no explicit sharing. You never need a "pointer to a map" in practice; the map itself already behaves like one.
Starter declares scores := make(map[string]int). In main:
- Add three entries:
"alice"→90,"bob"→85,"carol"→100. - Print
scores["bob"]on its own line. - Print
scores["dave"]on its own line (a key you did not add). You should see0. - Print
scoreson its own line.
Expected output:
85
0
map[alice:90 bob:85 carol:100]
(fmt.Println on a whole map sorts the keys alphabetically when it prints, so that third line is deterministic. Iteration order with range is a different story, covered two lessons from now.)
85 0 map[alice:90 bob:85 carol:100]