To check if a key exists in a Go map, use the comma-ok idiom: value, ok := m[key] — ok is a boolean that is true when the key is present and false when it is not. The reason this two-value form exists at all is that a single-value lookup cannot answer the question. m[key] on a missing key returns the zero value of the map's value type, which is indistinguishable from a key that is genuinely stored with that zero value. For a map[string]int, a lookup returning 0 could mean "the key maps to zero" or "the key is absent," and only ok tells you which.
The idiom, and why the second value matters
scores := map[string]int{"alice": 0, "bob": 42}
// Wrong: cannot distinguish "present with value 0" from "missing"
if scores["alice"] != 0 { /* skips alice, who really is present */ }
// Right: comma-ok
if v, ok := scores["alice"]; ok {
fmt.Println("present, value is", v) // this branch runs
} else {
fmt.Println("missing")
}
Alice is stored with value 0. The naive check treats her as absent; the comma-ok check gets it right. This is not a corner case — booleans default to false, ints to 0, strings to "", pointers to nil, so any map whose value type has a meaningful zero value is a trap for single-value lookups.
When you only care about presence and not the value, blank out the value with _:
if _, ok := scores["carol"]; ok {
// key exists; value irrelevant here
}
Nil maps read fine, but panic on write
A subtlety that catches people: reading from a nil map is legal and simply reports every key as absent. Writing to one panics.
var m map[string]int // nil, not initialized
v, ok := m["x"] // fine: v == 0, ok == false
m["x"] = 1 // panic: assignment to entry in nil map
So a comma-ok check never needs a nil guard, but any code path that inserts does. Initialize with make(map[string]int) or a literal before writing. This is a common source of nil-map panics in structs whose map field was never initialized in the constructor.
Building a set: map to struct
Go has no built-in set, and the idiomatic substitute is a map whose value type carries no data. Use struct{} rather than bool, because an empty struct occupies zero bytes:
seen := map[string]struct{}{}
seen["alice"] = struct{}{} // add
_, exists := seen["alice"] // check
delete(seen, "alice") // remove
map[string]bool also works and reads slightly cleaner (if seen[k]), at the cost of one byte per entry — fine for small sets, worth the struct{} for large ones. Either way, delete is safe to call on a key that is not present; it is a no-op, not an error.
Deleting and the "check before delete" non-issue
You do not need to check existence before deleting. delete(m, key) on an absent key does nothing and never panics, so the guard some people write is pure overhead:
// Redundant
if _, ok := m[k]; ok {
delete(m, k)
}
// Equivalent and cleaner
delete(m, k)
Concurrency: maps are not safe for parallel writes
This is where a map bug stops being a logic error and becomes a crash or a security problem. Go maps are not safe for concurrent use when at least one goroutine writes. The runtime actively detects concurrent map writes and deliberately crashes the program with "fatal error: concurrent map writes" — and unlike a panic, you cannot recover from it.
// Data race: two goroutines writing the same map -> runtime fatal error
go func() { m["a"] = 1 }()
go func() { m["b"] = 2 }()
An unguarded map behind an HTTP handler, hit by concurrent requests, is a denial-of-service waiting to happen — a burst of traffic takes the whole process down. Two standard fixes:
// 1. Guard with a mutex (RWMutex lets reads run in parallel)
var mu sync.RWMutex
mu.RLock(); v, ok := m[k]; mu.RUnlock()
mu.Lock(); m[k] = v; mu.Unlock()
// 2. sync.Map, for high-read / low-write or disjoint-key workloads
var sm sync.Map
sm.Store("k", 1)
v, ok := sm.Load("k") // sync.Map's own comma-ok equivalent
Reach for the mutex by default; sync.Map is specialized and often slower for the general case. Run tests and CI with the race detector (go test -race, go run -race) — it catches these before they reach production, which matters because concurrent-map crashes are timing-dependent and love to hide until load. The habit of building race and vulnerability detection into the pipeline is covered more broadly in the Safeguard Academy.
A compact reference
v, ok := m[k] // value + presence
_, ok := m[k] // presence only
m[k] = v // insert/update (panics if m is nil)
delete(m, k) // remove (safe if absent)
len(m) // number of keys
for k, v := range m {} // iterate (order is randomized by design)
Two closing reminders. Map iteration order is intentionally randomized in Go, so never rely on it — sort the keys if you need determinism. And the randomized order is a deliberate defense against a class of denial-of-service where predictable iteration or hashing lets an attacker force worst-case collisions.
FAQ
How do I check if a key exists in a Go map?
Use the comma-ok idiom: value, ok := m[key]. ok is true if the key is present. If you only need presence, discard the value with _, ok := m[key].
Why can't I just check if the value is zero?
Because a missing key returns the value type's zero value, which is identical to a key genuinely stored with that zero value. For map[string]int, a returned 0 is ambiguous; the ok boolean resolves it.
Is it safe to read from a nil map?
Yes. Reading a nil map returns the zero value with ok == false for every key. Writing to a nil map panics, so initialize it with make or a literal before any insert.
Are Go maps safe for concurrent access?
Only for concurrent reads. If any goroutine writes while others read or write, you get a data race and the runtime will fatally crash on detected concurrent writes. Guard the map with a sync.RWMutex or use sync.Map, and test with -race.