Go — panic: assignment to entry in nil map
Fix panic: assignment to entry in nil map by finding uninitialized maps, distinguishing read-safe nil maps from write-unsafe ones, and initializing before use.
Symptoms
- panic: assignment to entry in nil map with a stack trace pointing at the first m[key] = value write
- The program works until the code path that first writes to the map executes
- Reads from the same map never panic, including lookups on the zero-value map
Possible causes
- A struct field or variable of map type was declared but never initialized with make or a literal
- A function returns a map that is nil on some code paths (early error return) and callers write to it
- A nil map was passed as a parameter and the callee writes to it
- JSON or YAML unmarshalling into a nil map pointer leaves it nil when the input is empty
Diagnosis steps
Read the panic stack
go run ./cmd/app 2>&1 | head -8Interpretation
The frame at the top of the stack is the exact write that panicked; the enclosing function shows how the map was obtained.
Trace the map origin
grep -n "var .* map\[" src/*.goInterpretation
A declared map without make or a composite literal is the zero value nil; every write to it is a latent panic.
Check the nil-returning paths
grep -n "return nil, err" src/service.go | headInterpretation
Error paths returning a nil map set up the panic when callers skip the error check and write.
Root cause
The zero value of a Go map is nil. Reading a nil map is defined and returns the zero value, but writing to it panics because there is no underlying hash table to insert into. The program runs fine until the first write executes on the uninitialized map.
Fix
Initialize maps before writing with make(map[K]V) or a literal, prefer lazily initializing at the boundary that first writes, and check the error return of constructors instead of writing to the nil result. Use composite literals in struct constructors to guarantee fields are usable.
Risk note
A blanket init of every map at struct creation can hide paths that should have failed loudly. Initialize where the write happens or in the constructor that promises a usable value.
Verify the fix
Run the previously panicking path and assert it completes; add a test that writes through every constructor-returned map.
Symptom walkthrough
The panic message is a complete sentence about the crime:
panic: assignment to entry in nil map
goroutine 1 [running]:
main.(*Registry).Register(...)
/app/src/registry.go:31
The asymmetry is the teaching point: reads from a nil map are perfectly legal.
var m map[string]int
_ = m["k"] // fine: zero value, false
m["k"] = 1 // panic: assignment to entry in nil map
That is why the program passes tests that only look things up and panics the first time a write path runs — usually in a less-traveled branch like registration or caching.
Fix in practice
Initialize at the point of construction:
func NewRegistry() *Registry {
return &Registry{
entries: make(map[string]Entry), // usable immediately
}
}
Or lazily at the write boundary, which is the pattern for caches:
func (r *Registry) Register(key string, e Entry) {
if r.entries == nil {
r.entries = make(map[string]Entry)
}
r.entries[key] = e
}
And honor constructor errors — the panic is often the delayed consequence of ignoring a nil return:
reg, err := OpenRegistry(path)
if err != nil {
return err // otherwise reg is nil and the first write panics
}
Verification
Re-run the exact path that panicked — typically the registration or cache-write branch — and assert it completes and persists the entry. Then add a table-driven test that calls every constructor and immediately writes one key into the returned map, so a future nil-returning constructor fails in CI instead of production. For the lazy-init variant, a test that calls Register twice on a zero-value Registry proves both writes land. Remember the asymmetry while testing: a read from a nil map never panics, so lookup-only tests stay green — which is exactly why coverage must exercise a write path, not just reads.