Skip to content
gohacks.dev
Basics

Panic Assignment to Entry in Nil Map and How to Fix It

The nil map panic means you wrote to a map that was never made. Find which of four causes you have, from a bare var to a nested map, and fix it for good.

6 min read
Summarize in AI
Flat illustration of an editor window full of code.

The message is panic: assignment to entry in nil map, and it names its own cause. You assigned to a map that points nowhere. Go lets you declare a map variable without creating the table behind it, and everything except a write works on that empty shell, so the failure arrives later than the mistake. This page lists the four ways it happens, with the exact output, and how to make sure it cannot. The mechanics of why the zero value is nil are in zero values in Go, and the full behavior of maps is in the guide to maps.

Tested with Go 1.25.6, macOS on Apple Silicon (arm64)

The smallest reproduction

A declaration with var gives you the zero value, and the zero value of a map type is nil:

main.goGo
package main

import "fmt"

func main() {
	var scores map[string]int
	fmt.Println(scores == nil, scores["ada"], len(scores))

	scores["ada"] = 1
	fmt.Println(scores)
}
Text
true 0 0
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
	example.test/lab/main.go:9 +0xb8
exit status 2

The first print shows the situation: true 0 0. Reading scores["ada"] returns the zero value and len returns zero, so the program runs fine up to the assignment, where it stops with the panic and a stack trace pointing at the line. The trace is the one place to start. Look at the line number in your own code, which names the exact write, and work backwards to where that map variable was supposed to be created.

Cause 1: declared with var and never made

The program above is the plain case, and the fix is one of two forms. Both create a real map:

main.goGo
package main

import "fmt"

func main() {
	scores := make(map[string]int)
	scores["ada"] = 1

	ages := map[string]int{"grace": 45}
	ages["linus"] = 28

	fmt.Println(scores, ages)
}
Text
map[ada:1] map[grace:45 linus:28]

Use make when you start empty and a literal when you have initial entries. If you know roughly how many entries will arrive, make(map[string]int, n) saves rehashing. Do not write var m map[string]int for a map you will fill unless you assign a real map to m before the first write.

Cause 2: a struct field that nobody initialized

This is the form you hit in real programs. A struct has a map field, you create the struct with a literal or var, and the field is nil because the zero value of a struct zeroes every field:

main.goGo
package main

import "fmt"

type Registry struct {
	handlers map[string]func()
}

func (r *Registry) Add(name string, f func()) {
	r.handlers[name] = f
}

func main() {
	var r Registry
	r.Add("ping", func() {})
	fmt.Println(len(r.handlers))
}
Text
panic: assignment to entry in nil map

goroutine 1 [running]:
main.(*Registry).Add(...)
	example.test/lab/main.go:10
main.main()
	example.test/lab/main.go:15 +0x3c
exit status 2

The panic points inside Add, which looks innocent, because the missing step was in the caller or in the type's design. There are three fixes, and I choose based on who creates the type.

A constructor is the clearest and the Go convention for types that need setup. It guarantees every user gets a working value, and constructor functions in Go covers the naming:

main.goGo
package main

import "fmt"

type Registry struct {
	handlers map[string]func()
}

func NewRegistry() *Registry {
	return &Registry{handlers: make(map[string]func())}
}

func (r *Registry) Add(name string, f func()) {
	r.handlers[name] = f
}

func main() {
	r := NewRegistry()
	r.Add("ping", func() {})
	fmt.Println(len(r.handlers))
}
Text
1

Lazy initialization inside the writing method makes the zero value of the type usable, which fits the guidance in designing around a useful zero value:

Go
func (r *Registry) Add(name string, f func()) {
	if r.handlers == nil {
		r.handlers = make(map[string]func())
	}
	r.handlers[name] = f
}

The cost is a nil check on every write, and one more thing to remember if another method also writes. If a type has several writers, put the check in a private helper. Do not combine lazy initialization with concurrent use without a lock, because two goroutines can both see nil and both create a map.

The third fix is a struct literal that sets the field at the call site, Registry{handlers: map[string]func(){}}. It works, and it relies on every caller remembering, which is exactly what stopped working in the first place.

Cause 3: a map of maps

A nested map has an inner map per outer key, and the inner one does not exist until you create it. This code looks right and panics:

main.goGo
package main

import "fmt"

func main() {
	byUser := map[string]map[string]int{}
	byUser["ada"]["logins"] = 1
	fmt.Println(byUser)
}
Text
panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
	example.test/lab/main.go:7 +0x58
exit status 2

The outer map is made, so the write to byUser["ada"] would be fine. But byUser["ada"] evaluates to the zero value of map[string]int, which is nil, and the indexing assignment goes to that nil inner map. Create the inner map when you first need it:

main.goGo
package main

import "fmt"

func record(m map[string]map[string]int, user, event string) {
	inner, ok := m[user]
	if !ok {
		inner = make(map[string]int)
		m[user] = inner
	}
	inner[event]++
}

func main() {
	byUser := map[string]map[string]int{}
	record(byUser, "ada", "logins")
	record(byUser, "ada", "logins")
	record(byUser, "grace", "logins")
	fmt.Println(byUser)
}
Text
map[ada:map[logins:2] grace:map[logins:1]]

The helper reads with the two-value form, creates only when the key is missing and then increments through the inner map. If you do this often, consider a single map keyed by a struct, map[struct{ User, Event string }]int. It has one level, one allocation and no nil inner maps to forget. The reading pattern for nested lookups is in checking whether a key exists.

Cause 4: a package-level map that is assigned late

A package-level var cache map[string]string is nil until something assigns it. If the assignment lives in main or an init function that runs after another package's initializer has already written to it, you get the panic during startup. Initialize at the declaration, var cache = map[string]string{}, so no code can see the nil value. Keep in mind that a package-level map is shared by every goroutine, so it also needs a lock if anything writes after startup.

How to find which cause you have

Read the trace from the top. The first frame in your code names the line that wrote. Then ask which variable it indexed, and trace that variable back to its declaration. If it is a field, search for where the struct is created, and if it comes from a function that returns a struct, check whether that function sets the field. A quick check is to print m == nil just before the write. In a test, a constructor that returns a ready value, and a test that calls the first write on a freshly built value, catches the whole class.

Static analysis helps a little. go vet does not flag this, because a nil map is a valid value. Some linters flag a var m map[...] that is written in the same function. Treat those as hints rather than guarantees, since the dangerous cases span functions.

Version notes and recovery

The message has been the same for many releases, and it is a runtime panic, so recover can catch it. I would not. Catching it in a request handler and returning a 500 keeps the server up, and that can be reasonable as a last line of defense in a handler, but the bug is that the map was never created, and the fix is initialization. Write the test that reproduces it first, so the fix is proven. The Go specification on map types states that a nil map is equivalent to an empty map except that no elements can be added, and the Go blog's article on maps shows the same trap with the usual cure.

Taggederror fix
Share

Keep reading

Get new articles by email

We send one email when a new article is published. No spam.

Unsubscribe in one click. No spam, ever.