Check Whether a Key Exists in a Go Map the Right Way
Use the comma ok form to tell a missing map key from a stored zero value, with patterns for sets, nested maps and lookups that need a default.

Reading a map in Go never fails. If the key is not there, you get the zero value of the value type, with no error and no panic. That is why counts[word]++ works without setup, and it is also why a bare m[key] == 0 check is a bug waiting for the first real zero. Ask the map whether the key exists with a second return value.
Tested with Go 1.25.6, macOS on Apple Silicon (arm64)
The comma ok form
package main
import "fmt"
func main() {
stock := map[string]int{"apples": 12, "pears": 0}
fmt.Println(stock["pears"], stock["kiwis"])
n, ok := stock["pears"]
fmt.Println(n, ok)
n, ok = stock["kiwis"]
fmt.Println(n, ok)
if _, ok := stock["apples"]; ok {
fmt.Println("apples are listed")
}
}0 0
0 true
0 false
apples are listedThe first line prints 0 0 for two very different situations: pears is in the map with a count of zero, and kiwis is not in the map at all. The second and third show the difference, 0 true against 0 false. The last form, _, ok := m[key], is the idiom when you only care about membership. Scoping the check into the if statement keeps ok from leaking into the rest of the function, which also avoids the shadowing trap if you already have an ok or an err in scope.
Nothing here needs a non-nil map. Reading from a nil map is legal and reports the key as absent, which is the behavior the nil map panic leaves alone: only a write is a problem.
Sets and nested maps
A set in Go is a map whose values carry no information. The empty struct takes no space, and membership is the same two-value read:
package main
import "fmt"
func main() {
allowed := map[string]struct{}{"read": {}, "write": {}}
_, can := allowed["delete"]
fmt.Println(can)
users := map[string]map[string]bool{
"ada": {"admin": true},
}
fmt.Println(users["ada"]["admin"], users["bob"]["admin"])
if perms, ok := users["bob"]; ok && perms["admin"] {
fmt.Println("bob is an admin")
}
}false
true falseThe nested lookup users["bob"]["admin"] is safe to read because the missing outer key yields a nil inner map, and reading a nil map returns the zero value, false. But it cannot distinguish "bob has no entry" from "bob has an entry that says false". When that difference matters, check the outer key first, as the last if does. Writing is another matter: users["bob"]["admin"] = true panics because the inner map is nil, and the safe form creates it first.
When zero is a legitimate value
If your data can hold the zero value, the two-value form is not optional. A feature flag stored as false, a retry count of 0, an empty string. If you control the data and zero truly means "unset", a plain if m[key] != 0 is fine, and shorter. I use the plain form for counters and the comma ok form for everything else. A function that returns a default for missing keys is a small helper:
func get(m map[string]int, key string, fallback int) int {
if v, ok := m[key]; ok {
return v
}
return fallback
}Where this check is not enough
Existence and use are two separate steps, so on a map shared between goroutines the answer can be stale by the time you act on it. A check followed by a write needs one lock around both, or you will insert a duplicate: mutexes in Go covers the pattern. For slices, there is no key to check, and slices.Contains does a linear scan, which is the signal to switch to a map or a set when the list gets long. The full lifecycle of the type is in maps in Go without surprises, and the specification's index expressions define the comma ok behavior exactly.


