Creating Maps
How to create maps in Go using the make function and map literals, and understand the behavior of nil maps
A map variable in Go is just a reference until you explicitly initialize it. Without initialization, the map is nil — reading from it works, but writing crashes at runtime. This document covers the three ways a map can be created or exist: using make, using a map literal, and the nil map state. By the end, you’ll know exactly how to avoid the most common panic caused by writing to a nil map.
Creating Maps with make
The built‑in make function allocates and initializes a hash table under the hood and returns a map value that is ready for both reading and writing. Its signature is:
make(map[KeyType]ValueType, initialCapacity ...int) map[KeyType]ValueType
KeyType must be a comparable type (numbers, strings, pointers, structs of comparable fields, etc.). ValueType can be anything, including another map. The optional second argument is a capacity hint — an integer that tells Go how many entries you expect to put into the map.
// Without a capacity hint
scores := make(map[string]int)
scores["Alice"] = 92
fmt.Println(scores["Alice"]) // 92
A map created this way is empty but not nil. You can immediately assign values to it. If you plan to insert a known number of entries, supplying a capacity hint helps the runtime avoid rehashing as the map grows.
// Pre-allocate space for ~200 key-value pairs
catalog := make(map[string]float64, 200)
The capacity hint is an optimization, not a hard limit. You can add more than 200 entries; the map will still grow, but the initial allocation reduces early growth‑related work. For short‑lived, small maps the hint can usually be omitted.
Ready to use:
Using make guarantees the map is fully initialized. Both reads and writes are safe immediately after the call.
Capacity is not a limit:
The capacity hint is purely a performance tuning knob. The map can always hold more elements than the hint; exceeding it simply triggers an internal resize later, which is transparent to your code.
A subtle but important detail: make always returns a non‑nil map. If you later assign nil to the same variable, you’ll replace the initialized map with a nil one, and writes will panic again. This normally happens only by mistake, but it’s worth knowing when debugging.
Creating Maps with Map Literals
A map literal creates a fully initialized map with an optional set of key‑value pairs in one expression. The syntax mirrors composite literals for slices and structs:
m := map[KeyType]ValueType{
key1: value1,
key2: value2,
// trailing comma required for multi-line
}
An empty literal map[K]V{} is functionally identical to make(map[K]V) — it produces an initialized, non‑nil map that is safe for writes.
capitals := map[string]string{
"France": "Paris",
"Japan": "Tokyo",
}
fmt.Println(capitals["Japan"]) // Tokyo
Literal syntax shines when you already know the initial data. Instead of making an empty map and calling several assignments, you express it all at once. This is common for lookup tables, configuration maps, and test fixtures.
Empty literal equals make:
m := map[string]int{} creates an empty, non‑nil map. It is equivalent to m := make(map[string]int) and can be used interchangeably.
One gotcha with literals is the required trailing comma on multi‑line declarations. Forgetting it causes a compile‑time error, not a subtle runtime bug. Single‑line literals do not need the trailing comma.
// Single-line: no trailing comma needed
m := map[string]int{"x": 1, "y": 2}
// Multi-line: trailing comma required on the last pair
m := map[string]int{
"x": 1,
"y": 2,
}
If you accidentally write to a variable that still holds a nil map because the literal was never assigned, you’ll hit the same nil‑map panic described next. Always verify that the variable receiving a literal actually gets it — for example, a global var m map[string]int that is meant to be initialized in init() but wasn’t.
The Nil Map
A map declared but not initialized has the zero value nil.
var m map[string]int // m == nil
A nil map behaves like an empty map for reads: indexing it returns the zero value of the value type, and the length is 0.
var m map[string]int
fmt.Println(m["anything"]) // 0
fmt.Println(len(m)) // 0
This allows you to read from a map field in a struct that hasn’t been set up yet without panicking. Code that only queries a map can often work with a nil map without extra checks.
Writing to a nil map causes a runtime panic.
var m map[string]int
m["key"] = 1 // panic: assignment to entry in nil map
Write to nil map crashes the program:
Any assignment to a nil map is a fatal runtime panic. The program stops immediately unless recovered from, which is rarely the right approach for this error.
This panic is a common source of mistakes. It often appears when a map is declared as a field in a struct and the struct is created without explicitly initializing the map. For example:
type Config struct {
Settings map[string]string
}
c := Config{} // c.Settings is nil
c.Settings["host"] = "localhost" // panic
The fix is to initialize the map before writing, either in the constructor or right after struct creation:
c := Config{Settings: make(map[string]string)}
c.Settings["host"] = "localhost" // safe
Always initialize before writing:
Whether you use make, a literal, or copy an existing map, you must ensure the map is non‑nil before any assignment. A nil map silently accepts reads; it’s the write that reveals the bug.
Nil maps aren’t inherently dangerous. They can be intentional placeholders: a struct field that remains nil until it’s needed, saving the cost of allocation for paths that never use the map. You just need to know when you’re crossing from read to write and initialize accordingly.
Checking for nil is straightforward:
if m == nil {
m = make(map[string]int)
}
m["key"] = 1
The distinction between a nil map and an empty initialized map is important: the zero‑value behavior on reads is convenient, but the write panic is the guardrail that forces you to think about initialization early.