Map Fundamentals
Understand what maps are in Go, how to declare a map type, and the different ways to create and initialize maps for key-value storage.
A map is Go’s built-in key-value store. You put a key and a value together, and later you can retrieve the value instantly using that same key. If you’ve used a dictionary in Python, a HashMap in Java, or an object in JavaScript, the idea is the same — but Go’s maps are designed to be simple and explicit about what types of keys and values they hold.
At its core, a map is a reference to a hash table. The runtime takes your key, runs it through a hash function, and determines exactly where in memory the corresponding value lives. That’s why lookups, inserts, and deletes all happen in constant time on average. The trade-off is that maps are unordered — the runtime decides where everything goes, not you.
This page covers the two things you need to know before you can use a map at all: how to write a map type, and how to turn that type into a real, working map.
Unordered Collection:
Maps do not maintain insertion order. The order of key-value pairs when iterating with for range is intentionally unpredictable and can vary between runs. If you need a stable order, you must track keys separately in a slice.
Map Type and Declaration
A map variable’s type spells out exactly what kinds of keys and values it can store. The syntax is map[KeyType]ValueType, where KeyType is the type of the keys and ValueType is the type of the values. The key type must be comparable — meaning Go can tell whether two keys are equal using ==. All basic types (integers, floats, strings, booleans), pointers, channels, and structs or arrays composed entirely of comparable types are allowed. Slices, maps, and functions cannot be keys because they are not comparable.
var ages map[string]int
var flags map[bool]string
var counts map[int]float64
A declaration like var ages map[string]int creates a map variable named ages whose zero value is nil. The variable exists, but there is no underlying data structure behind it yet. You can read from a nil map — it behaves like an empty map, returning the zero value for the value type. But you cannot write to it. Attempting to store a key-value pair into a nil map triggers a runtime panic.
var ages map[string]int
fmt.Println(ages["missing"]) // 0, no error
ages["Alice"] = 30 // panic: assignment to entry in nil map
This is the single most common mistake beginners make with maps — declaring a variable and then trying to assign to it without initializing. The compiler won’t stop you; the panic only happens at runtime.
Maps are reference types, like slices and pointers. When you assign one map variable to another, both variables point to the same underlying hash table. Changes made through one variable are visible through the other. We’ll see more of this when we discuss working with maps, but for now, the important takeaway is that a map variable is lightweight — it’s just an 8-byte pointer on 64-bit machines, or 4 bytes on 32-bit machines — and copying the variable does not copy the data.
Panic on nil map write:
Writing to a nil map causes the runtime panic assignment to entry in nil map. Always initialize a map before storing values — either with make or with a literal that provides at least an empty underlying structure.
Creating Maps
A declared map variable is nil and not yet usable for writes. To create a working map, you must initialize it. Go gives you two primary ways: the built-in make function and a map literal. Both allocate the internal hash table and return a non-nil map value ready for use.
Using make
The make function allocates and initializes a map. It takes the map type and an optional capacity hint:
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25
Without a capacity argument, make creates a map that can grow dynamically as needed. If you already know roughly how many entries you’ll store, you can supply an initial capacity to reduce reallocation overhead:
scores := make(map[string]int, 100)
The capacity argument is a hint, not a hard limit — the map can still grow beyond it. Specifying it can improve performance when you’re building a large map incrementally, but it’s never required for correctness.
Using a Map Literal
A map literal lets you declare and populate a map in one expression. Inside curly braces, you list key-value pairs separated by colons:
ages := map[string]int{
"Alice": 30,
"Bob": 25,
"Eve": 35,
}
The trailing comma after the last entry is required when the closing brace is on a new line — a small syntactic detail that trips up newcomers occasionally.
An empty map literal allocates a usable, empty map that is not nil:
empty := map[string]int{}
empty["later"] = 42 // works fine
This is functionally equivalent to make(map[string]int) but expressed as a literal. Both give you a non-nil map ready for reads and writes.
Map Ready:
Once a map has been initialized — whether through make or a non-nil literal — you can safely read from it and write to it. If you can print the map and see the values you stored, the map is correctly set up.
Nil vs. Empty Map
A nil map (var m map[string]int) has no underlying hash table. An empty map created with make or map[string]int{} does. The difference matters only when you write to it. Reading from both works identically, returning the zero value for missing keys. Because a nil map can’t grow, it also doesn’t allocate any memory for storage — so a nil map may be slightly cheaper as a placeholder if you’re certain you’ll never write to it.
Maps Are Reference Types
Once a map is initialized, assigning it to another variable shares the same underlying data:
original := map[string]int{"x": 1}
alias := original
alias["x"] = 2
fmt.Println(original["x"]) // 2
Both original and alias refer to the same hash table. If you need an independent copy, you must create a new map and iterate over the old one to copy entries. The same reference semantics apply when you pass a map to a function — the function can modify the original map’s contents.
Maps cannot be compared with ==:
The equality operators == and != cannot be applied to map values, except when comparing a map to nil. Attempting to compare two maps — even two that contain identical keys and values — results in a compile-time error. To test deep equality, use reflect.DeepEqual or write your own comparison loop.
Summary
Map type declarations describe what you intend to store, but they don’t create storage. Until you call make or use a literal, your map variable is nil and dangerous to write to. The initialization step is where the underlying hash table comes to life — and after that, the map behaves like a flexible, fast key-value store with reference semantics.