Fix

"assignment to entry in nil map"

Fix 'assignment to entry in nil map' by initializing the map with make() before assigning values to it.

The map variable points to nothing

You declare a map variable, assign a value to a key, and the program crashes. The runtime prints assignment to entry in nil map. The code compiles without warnings. The types match. The logic makes sense. Go stops execution because the map variable holds a nil reference. You have a name for a map, but the underlying data structure does not exist.

This panic is one of the most common runtime errors for Go developers coming from languages that auto-initialize collections. Go does not create the map automatically. The zero value of a map is nil. You must allocate the map before you can store data in it.

Maps are references to hash tables

A map in Go is a reference type. The variable holds a reference to an underlying hash table structure managed by the runtime. When you declare a map variable without initializing it, the variable holds nil. Nil means the reference points to no hash table.

Assigning a value requires Go to compute a bucket index and store the key-value pair. Without a hash table, there is nowhere to put the data. The runtime detects the nil reference and panics to prevent memory corruption.

Think of a map variable as an address label. Declaring the variable gives you the label. Initializing the map builds the house at that address. You can't deliver mail to an address where no house exists.

package main

import "fmt"

func main() {
    // Declare a map variable.
    // The zero value is nil. No hash table exists yet.
    var scores map[string]int

    // This panics at runtime.
    // scores["alice"] = 100 // PANIC: assignment to entry in nil map

    // Initialize the map with make.
    // make allocates the hash table and returns a reference.
    scores = make(map[string]int)

    // Now assignment works.
    scores["alice"] = 100
    fmt.Println(scores)
}

Initialize maps with make. Trust the zero value for reads.

What happens at runtime

The compiler allows the assignment because the types are correct. scores is a map[string]int, and you are assigning an int to a string key. The type checker has nothing to complain about. The crash happens when the code runs.

At runtime, the assignment operation checks the map header. If the header is nil, the runtime triggers a panic. The panic message is assignment to entry in nil map. The message includes a stack trace pointing to the exact line of code. The top frame shows the function and line number where the assignment occurred.

This behavior is consistent across all map operations that modify the structure. You cannot assign a value, delete a key, or range over a nil map if the operation requires the hash table. Reading a value is the exception. Reading from a nil map is safe. It returns the zero value for the element type.

package main

import "fmt"

func main() {
    var m map[string]int

    // Reading from a nil map is safe.
    // It returns the zero value of the element type.
    val := m["missing"]
    fmt.Println(val) // prints 0

    // The comma-ok idiom also works on nil maps.
    // ok is false because the key is not present.
    val, ok := m["missing"]
    fmt.Println(val, ok) // prints 0 false
}

Reading from a nil map returns the zero value. It does not panic. This design allows you to check for existence without initialization overhead.

Realistic scenario: struct fields

This bug often hides inside structs. When you create a struct, map fields start as nil. You must initialize them before use. This is a frequent source of panics in configuration loaders, caches, and data processors.

package main

import "fmt"

type Config struct {
    // Settings is a map field.
    // It starts as nil when the struct is created.
    Settings map[string]string
}

func loadConfig() *Config {
    // Struct literal creates the struct.
    // Fields take their zero values.
    // Settings is nil.
    c := &Config{}

    // This panics. Settings is nil.
    // c.Settings["debug"] = "true"

    // Fix: Initialize the map.
    c.Settings = make(map[string]string)
    c.Settings["debug"] = "true"
    return c
}

func main() {
    cfg := loadConfig()
    fmt.Println(cfg.Settings)
}

A cleaner approach is to initialize the map inside the struct composite literal. This keeps allocation and assignment atomic. The map is ready to use immediately after the struct is created.

package main

import "fmt"

type Config struct {
    Settings map[string]string
}

func loadConfig() *Config {
    // Initialize the map in the composite literal.
    // make creates the hash table.
    // The map is non-nil and ready for assignment.
    c := &Config{
        Settings: make(map[string]string),
    }

    c.Settings["debug"] = "true"
    return c
}

func main() {
    cfg := loadConfig()
    fmt.Println(cfg.Settings)
}

Initialize map fields in the struct literal. Avoid nil maps inside structs unless the field is optional and read-only.

Why Go does not auto-initialize

Some languages create an empty map automatically when you declare a variable. Go does not. The zero value of a map is nil. This is a deliberate design choice.

Auto-initialization hides allocation costs. If Go created a map automatically, every map variable would consume memory for the hash table structure. The runtime would allocate buckets and metadata even if you never stored a value. With nil as the zero value, you pay for the table only when you need it. This keeps memory usage predictable and explicit.

It also forces you to think about initialization. You cannot accidentally use a map without allocating it. The compiler catches type errors, but initialization is a runtime concern. The panic makes the bug obvious. You see the error immediately and fix it by adding make.

Go applies the same rule to slices and channels. Slices and channels also require make. The pattern is consistent: reference types need initialization. Arrays and structs do not. They are value types and contain their data directly.

Maps are references. Nil references point to nothing.

Capacity hints and performance

make accepts a second argument for capacity hint. You can pass the expected number of elements to reduce resizing overhead.

package main

import "fmt"

func main() {
    // Create a map with capacity hint for 100 elements.
    // The runtime pre-allocates space for roughly 100 entries.
    m := make(map[string]int, 100)

    for i := 0; i < 100; i++ {
        m[fmt.Sprintf("key%d", i)] = i
    }

    fmt.Println(len(m))
}

Maps grow dynamically. When the load factor gets too high, the map resizes. Resizing copies all entries to a larger table. This takes time and allocates new memory. Providing a capacity hint reduces the number of resizes. If you know the map will hold 1000 items, pass 1000 to make. The performance gain is noticeable in tight loops or when processing large datasets.

Do not over-allocate. If you pass a huge capacity hint but only store a few items, you waste memory. The hint is a suggestion. The runtime uses it to estimate the initial size. Use the hint when you have a good estimate of the map size.

Use make(map[K]V, hint) when you know the map will grow large. Use make(map[K]V) when the size is unknown or small.

Pitfalls and safe operations

Only assignment panics on a nil map. Other operations are safe.

Reading a key returns the zero value. val := m["key"] sets val to 0 for integers, "" for strings, or nil for pointers. The comma-ok idiom works too. val, ok := m["key"] sets ok to false. This allows you to distinguish missing keys from zero values even on nil maps.

Deleting a key is safe. delete(m, "key") does nothing on a nil map. It does not panic. This is useful for cleanup code that might run on uninitialized maps.

Ranging over a nil map is safe. The loop body never executes. for k, v := range m iterates zero times. This allows you to write generic iteration code without checking for nil.

The panic occurs only when you try to modify the map. Assignment is the most common trigger. m["key"] = value panics. delete does not. len(m) returns 0. cap is not defined for maps.

The worst map bug is the one that never logs. Always initialize maps before writing.

Decision matrix

Use make(map[K]V) when you plan to assign values to the map. make allocates the hash table and returns a ready-to-use reference.

Use a nil map when the map is read-only or serves as a zero value. Reading from a nil map returns the element's zero value without panicking.

Use map[K]V{} when you want to initialize the map and add entries in a single expression. The composite literal calls make internally and populates the map.

Use a struct composite literal with make when the map is a field. Initialize the map inside the struct literal to keep allocation and assignment atomic.

Use make(map[K]V, hint) when you know the map will grow large. The capacity hint reduces resizing overhead and improves performance.

A nil map is a safe zero value. It is not a writable container.

Where to go next