Looking up a key that isn't in a Go map doesn't crash and doesn't return null — it silently hands back the value type's zero value, which is convenient right up until it quietly hides a real bug.
2 min read
ages := map[string]int{"Ada": 36, "Grace": 34}
fmt.Println(ages["Ada"]) // 36
fmt.Println(ages["Nobody"]) // 0 — no error, no panic, no nullLooking up "Nobody", a key that was never set, doesn't panic and doesn't
return some Go equivalent of null — it returns int's zero value (the
same zero-value concept from the types lesson), which is 0. This is
convenient for writing short code without constant existence checks, but
it creates a real ambiguity: you can't tell, from that return value alone,
whether the key was missing or whether it genuinely exists and its value
happens to be 0.
Go's map lookup has a second form that returns an explicit boolean alongside the value, resolving the ambiguity directly:
value, ok := ages["Nobody"]
fmt.Println(value, ok) // 0 false — ok tells you the key wasn't there
value, ok = ages["Ada"]
fmt.Println(value, ok) // 36 trueok is true if the key actually existed, false if it didn't — this is
the idiomatic, reliable way to distinguish "the key is genuinely mapped to
the zero value" from "the key was never set at all," and it's a pattern
worth reaching for by default anywhere the distinction actually matters.
stock := map[string]int{"apples": 0, "pears": 12}
if qty, ok := stock["apples"]; ok {
fmt.Println("we track apples, quantity:", qty) // runs: the key exists, its value just happens to be 0
} else {
fmt.Println("we don't carry apples")
}Returning a zero value instead of forcing an existence check on every single
lookup keeps common code much shorter. Counting occurrences, for instance,
needs no check at all — a missing key reads as 0, so adding one just works
the very first time too:
wordCounts := map[string]int{}
wordCounts[word]++ // a missing key reads as 0, so this stores 1 the first timeGo trusts you to use the two-value form only when the distinction between "missing" and "zero" actually matters for your specific logic, rather than forcing every lookup everywhere to be more verbose than it needs to be.
A bug where a counter, a running total, or a cache "just happens" to
silently start at 0 for a key that was never actually initialized is a very
common Go-specific mistake, precisely because the map lookup doesn't fail
loudly the way an out-of-bounds access or a null-pointer dereference would
in other languages. Reaching for the value, ok := form whenever "did this
key actually exist" is a meaningful question is the concrete habit this
lesson is building.
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. What happens when you look up a key that doesn't exist in a Go map?
2. Why is this zero-value behavior ambiguous in some cases?
3. What does the two-value form `value, ok := myMap[key]` provide that the single-value form doesn't?
Go