Last Updated: May 17, 2026
A map is Go's built-in way to look something up by name. When you need to find a product's price from its product code, count how many of each item are in a shopping cart, or check whether an email already exists in your customer list, a map is the right tool. This lesson covers what a map is, how to declare and create one, which types are allowed as keys, and how maps behave when you assign them between variables.
A map stores pairs of values. Each pair has a key and a value. You hand the map a key, and it gives you back the value associated with that key. The classic example is a price list: the product code is the key, the price is the value.
The type is written map[K]V, which reads "map with keys of type K and values of type V". In the example above, the type is map[string]float64: keys are strings, values are floats. Both types are part of the map's type, so a map[string]float64 and a map[string]int are different types and can't be mixed.
Behind the scenes Go implements a map as a hash table. That means looking up a value by key is fast: on average, the time to find one item doesn't depend on how many items the map holds. You don't need to know how the hash table works to use a map well, but you should know that lookups are quick and that the order of items in a map is not predictable. For now, picture a map as a lookup table.
Three things are true of every map. Every key has the same type, every value has the same type, and each key appears at most once. Putting a value at a key that already exists replaces the old value.
Cost: map lookup, insert, and delete are O(1) on average. Compare that to scanning a slice for a matching name, which is O(n) per lookup. If your code does many lookups by key, a map is almost always the right shape.
The most direct way to declare a map without initializing it is with var:
This declares prices but does not allocate a map for it. The zero value of a map type is nil. A nil map prints as map[], has length 0, and compares equal to nil. The variable exists, but there's no actual hash table behind it yet.
A nil map is usable for some things and dangerous for others. Reading from a nil map is fine, you'll just get the zero value of the value type back. Writing to a nil map is a runtime panic.
The first line is safe because reading from a nil map returns the value type's zero value, 0 for float64. The second line crashes because Go won't silently create a hash table for you the first time you write. You have to allocate one explicitly.
This is the most common map mistake. Forgetting to allocate produces a panic on the first write, and the error message is exactly the one above.
What's wrong with this code?
cart is declared but never allocated, so it's nil. The line cart["notebook"] = 2 tries to write into a map that doesn't exist yet and triggers panic: assignment to entry in nil map.
Fix:
Either use a map literal (map[string]int{}) or make to allocate a real map before writing to it.
makeTo get a usable, empty map, call the built-in make function with the map type:
make(map[string]int) allocates a hash table and returns a non-nil map ready for writes. There's no panic on the first insert, and len reports the count as items are added.
make accepts an optional second argument: a hint for how many items you expect to store. It doesn't put a cap on the map, it just lets the runtime size the hash table up front to avoid resizing later.
The hint is a hint, not a limit. The map starts at length 0 regardless, and you can still add more than 100 items if you want. The only effect is to skip some internal resizing work as the map grows.
Cost: if you know roughly how many items will end up in the map, passing a size hint to make avoids the rehashing work that happens as the map grows. For small maps this is invisible. For maps that fill with thousands of items in a tight loop, it can be a meaningful win.
A map literal lets you declare a map and fill it in at the same time. The syntax mirrors a struct literal, with each entry written as key: value separated by commas:
Notice the trailing comma after 2.00 on its own line. Go requires it whenever the closing brace is on a new line. This is gofmt's rule, and it lets you reorder, add, or remove entries without touching neighboring lines.
The output order is not the insertion order. Maps do not keep entries in the order you put them in. Each program run may print them in a different order. For now, do not assume any particular order from a map.
You can also write an empty map literal, which is the literal-style version of make(map[K]V):
map[string]int{} allocates an empty, non-nil map. It's a common starting point when you plan to fill the map later in the program. Most Go developers use it for empty maps and reserve make for the cases where they want to pass a size hint.
Here's the practical summary of the four ways you've seen so far to start a map:
| Form | Result | Safe to write to? |
|---|---|---|
var m map[string]int | nil map | No, panics on write |
m := map[string]int{} | Empty, non-nil | Yes |
m := make(map[string]int) | Empty, non-nil | Yes |
m := make(map[string]int, 100) | Empty, non-nil, pre-sized | Yes |
The first one is useful when you want a "no map yet" sentinel value or when a function might return without ever assigning a real map. The other three are the everyday choices for "give me a map I can use right now".
Not every Go type can be used as a map key. The rule comes from how maps work internally: the runtime hashes the key to find a bucket, then compares keys with == to confirm a match. So a key type must support ==. Go calls such types comparable.
These types are comparable and work as map keys:
| Type | Example | Notes |
|---|---|---|
| Strings | map[string]int | The most common choice |
| Integers and floats | map[int]string, map[float64]bool | Float keys are rare; avoid because NaN != NaN |
| Booleans | map[bool]int | Only two possible keys, rarely useful |
| Pointers | map[*Product]int | Compares by address, not by what they point at |
| Channels | map[chan int]string | Compares by identity |
| Arrays of comparable types | map[[3]int]string | Fixed-size arrays only, not slices |
| Structs of comparable fields | map[Address]Customer | Every field must itself be comparable |
| Interfaces | map[any]int | Stores comparable values; panics at runtime if a non-comparable type is inserted |
And these types are not comparable, so they can't be used as keys:
[]int, []string, etc.). Slices can only be compared to nil, not to each other.==.The compiler catches this at build time. Slice keys would force the runtime to define equality between slices, which Go intentionally doesn't.
Strings are the workhorse key type, and most maps you'll write use them. Integers come up for things like "score by customer ID" or "count by quantity bucket". Structs as keys are useful when an item is naturally identified by more than one field together.
Two Address values are equal when their City and ZipCode fields are equal, so the lookup finds the right entry. If you tried to put a slice field inside Address, this whole map type would become illegal and the compiler would refuse to build it.
There's no restriction on the value type. Anything goes: numbers, strings, slices, other maps, structs, functions, channels, interfaces. A map of customer names to their order history slice is fine. A map of category names to a nested map of products is fine. A map of strings to functions is fine.
The value type here is []string, a slice. Each customer has their own slice of order IDs, and the map gives you O(1) access to that slice by email.
You can also nest maps, which is useful when an item needs two keys to identify it. A common shape is "category, then product, then price":
The outer map's value type is itself a map (map[string]float64). Two indexing operations chain naturally. Nested maps are powerful, but they get awkward fast, especially when an inner map might not exist yet. Once you've seen structs, a map of structs is usually clearer than two nested maps.
A map variable doesn't hold the map's contents directly. It holds a small internal handle that points at the actual hash table, which lives somewhere else in memory. When you assign a map to another variable, or pass it to a function, the handle is copied, but both copies still refer to the same underlying hash table. Writes through one variable show up through the other.
The line copyOfCart := cart looks like it copies the map, but it doesn't. Both variables point at the same hash table, so the new entry shows up in cart too. This is the same behavior slices have, and it's the meaning of "reference type" in Go.
The same thing happens when a function receives a map parameter. The function can add, change, or delete entries and the caller sees the changes:
Notice that addItem doesn't return anything. It mutates the caller's map directly. This is the standard Go pattern for "here's a map, please update it for me", and it works because the map header is shared, not copied.
Here's a Mermaid view of what's happening. Two variables, one hash table.
Both cart and copyOfCart hold separate handles, but those handles point at the same data. There's only ever one hash table in this picture. Adding an entry through either variable adds it to the single shared table, which is why both variables see the new entry.
This sharing has a useful consequence and a sharp edge. The useful side is that you can hand a map to a helper function and let it fill in entries without returning anything. The sharp edge is that if you actually want an independent copy of a map, the plain = won't give you one. You have to build a new map and copy each entry across, or use maps.Clone from the maps standard library package.
lenThe built-in len function works on maps the same way it works on slices. It returns the number of key-value pairs currently in the map, as an int.
len reports the count of keys, not the sum of values. The cart above has 3 keys, even though the quantities add up to 8. A nil map has length 0, and len is safe to call on a nil map without panicking.
Cost: len(m) is O(1). The runtime keeps a count alongside the hash table, so asking for the length never walks the entries.
Here's a small program that uses what's been covered so far. It builds a price list, looks up a few items, demonstrates that an unknown key returns the zero value, and shows that two variables can share the same underlying map.
Three things to notice. First, len(prices) is 3 because there are three keys. Second, asking for "stapler" doesn't panic, it returns 0.0, the zero value of float64. That's convenient when zero is a sensible default, and trouble when zero is also a valid stored value. Third, the assignment clone := prices did not copy the map. Writing through clone updated the same hash table that prices reads.
map[K]V. It's implemented as a hash table, so lookups, inserts, and deletes are O(1) on average.nil. A nil map is safe to read from and to call len on, but writing to one panics with assignment to entry in nil map.map[K]V{...}, an empty literal map[K]V{}, or make(map[K]V). Pass a size hint as a second argument to make when you know the rough final size.len(m) returns the number of key-value pairs, in constant time. It's safe on a nil map and returns 0.The next lesson, Map Operations (CRUD), covers the four core operations on a map: inserting new entries, reading values back, updating existing entries, and deleting keys. It also shows the common patterns that combine them.