Go Interview Prep #5: Maps
Maps are Go’s built-in hash table. They are fast and easy to use, but they have several sharp corners that interviews frequently probe.
Creating a map
// Using make (preferred for non-literal maps)
m := make(map[string]int)
// Optional size hint (capacity is not a hard limit)
m := make(map[string]int, 100)
// Map literal
m := map[string]int{"a": 1, "b": 2}
// Zero value — nil, not empty
var m map[string]int // m == nil
The size hint to make reduces reallocations when you know the approximate number of keys up front; it is not a maximum.
Nil map behavior
A nil map reads like an empty map but panics on write:
var m map[string]int
_ = m["key"] // ok: returns 0
m["key"] = 1 // panic: assignment to entry in nil map
Always initialize a map before writing to it. len(nilMap) is 0; ranging over a nil map is a no-op.
The comma-ok idiom
A map lookup returns a zero value when the key is absent. Use the two-value form to distinguish “key not present” from “key present with zero value”:
v, ok := m["key"]
if !ok {
// key does not exist
}
Deletion
delete(m, "key") // safe even if key doesn't exist; no-op on nil map
Iteration order is random
Go deliberately randomizes map iteration order to prevent code from depending on it:
for k, v := range m {
fmt.Println(k, v) // order varies each run
}
If you need sorted output, collect the keys into a slice and sort them first (slices.Sort / sort.Strings).
You may safely delete the current key during a range loop; deleting other keys or adding keys has unspecified effects on remaining iteration.
Key types must be comparable
Only types that support == can be map keys: booleans, numbers, strings, pointers, channels, interfaces, arrays, and structs (if all fields are comparable). Slices, maps, and functions cannot be keys.
Interface keys are allowed, but if the dynamic type is incomparable (e.g. a slice), using that key panics at runtime. Prefer concrete comparable key types.
Maps are not safe for concurrent use
Concurrent reads of a map that is not being written are fine. A concurrent read and write — or two concurrent writes — triggers a fatal runtime error (concurrent map read and map write / concurrent map writes). That is not a recover-able panic; the process exits.
// Detected as a fatal concurrent map access (and by the race detector)
go func() { m["x"] = 1 }()
go func() { _ = m["x"] }()
Options:
- Wrap with
sync.Mutex/sync.RWMutexfor general use. - Use
sync.Mapfor read-heavy, write-once (or disjoint-key) workloads. - Shard the map (multiple maps + locks) under high contention.
Map internals (brief)
Go maps are hash tables with bucket-based chaining (8 key/value slots per bucket, overflow buckets as needed). Average lookup, insert, and delete are amortized O(1). The map grows when the load factor exceeds about 6.5 entries per bucket; existing entries are incrementally evacuated to the new buckets during subsequent operations (so a single insert can do a bit of extra work, not a full stop-the-world copy of the whole map).
Common interview questions
Q: What happens when you read a missing key?
You get the zero value for the value type — 0 for int, "" for string, nil for pointers and slices. Use the comma-ok form to distinguish absence from a stored zero.
Q: Why is map iteration order random?
Intentional design decision: it forces developers not to rely on ordering. In early Go versions iteration order was accidentally deterministic, which led to bugs when the runtime changed.
Q: Can you take the address of a map value?
No. Map values are not addressable (&m[k] does not compile). To mutate a struct stored as a map value, read it out, modify it, and write it back — or store pointers (map[string]*Counter).
type Counter struct{ N int }
m := map[string]Counter{}
c := m["x"]
c.N++
m["x"] = c
Q: How do you implement a set in Go?
Use map[T]struct{}. Empty struct{} values consume no memory.
set := make(map[string]struct{})
set["go"] = struct{}{}
_, exists := set["go"]
Q: What is the difference between a nil map and an empty map?
Both have len == 0 and support reads. Only the empty (initialized) map allows writes. Prefer make or a literal before first write; return nil maps from functions when “no data” is fine for read-only callers.
Quick reference
| Operation | Behavior |
|---|---|
m[k] (missing key) | Returns zero value |
v, ok := m[k] | ok is false if key absent |
delete(m, k) | Safe even if key absent |
| Nil map read | Returns zero value |
| Nil map write | Panic |
| Concurrent read+write | Fatal runtime error (use mutex or sync.Map) |
| Iteration order | Randomized |
| Map value address | Not addressable |
