Last Updated: May 17, 2026
A map is only useful once you can put data into it, pull data out, change what's there, and remove what you don't need anymore. Go covers all four with a small set of operations: index assignment, index read, the delete built-in, and len. The syntax is short, but a handful of behaviors catch newcomers off guard, especially what happens when you ask for a key that isn't there and what happens when you write to a nil map. This lesson works through each operation in turn, then closes with the two patterns you'll write most often: counting and lookup-then-update.
You add an entry to a map with the index assignment syntax: m[key] = value. The map grows by one entry, and the value is stored under that key.
Three writes, three entries. The print order isn't tied to insertion order, but the contents are exactly what you'd expect.
Insert is also how you replace a value. If the key already exists, the new value overwrites the old one. There's no separate "update" operation in Go's map API. Both insert and update use the same m[key] = value syntax, and the map figures out which one is happening based on whether the key already has an entry.
Two writes, two existing keys, two value updates. The map still has only two entries.
Cost: Map writes are amortized O(1). Most writes hash the key and update one slot. When the underlying hash table runs out of room, Go grows it and rehashes the entries, which is O(n) for that one call but rare enough that the average per-write cost stays constant.
You read a value with the same index syntax used for arrays and slices: v := m[key]. The result is the value stored under that key.
Reading a key that exists is uneventful. The interesting case is what happens when you read a key that isn't in the map.
The map doesn't have "Marker", and yet the read succeeded and gave back 0. Go's map read never fails. When the key isn't present, the read returns the zero value of the value type. For int, that's 0. For string, it's "". For a pointer, slice, map, or interface, it's nil. For a struct, it's a struct with every field set to its own zero value.
This is convenient for some patterns (the counting pattern at the end of this lesson leans on it directly), but it's also a trap. A returned 0 could mean "the key exists and its value is 0" or "the key doesn't exist." From the value alone, you can't tell. Go solves this with the comma-ok idiom: count, ok := stock["Marker"] gives you both the value and a boolean that's true only when the key was actually present. For now just remember that a zero-valued read doesn't prove the key is missing.
The second print shows the zero value for float64, which is 0. The map didn't grow, didn't error, and didn't panic. It just gave back the zero value and carried on.
Cost: Map reads are O(1) on average. Each read hashes the key and looks at one bucket. Worst case is O(n) if many keys collide into the same bucket, but Go's hashing is randomized per process to keep that worst case out of reach in normal code.
The update syntax is identical to insert: m[key] = newValue. Go's map API doesn't distinguish the two operations because it doesn't need to. The result is the same shape either way: after the call, m[key] evaluates to newValue.
The cart still has two entries. Only the value under "Pen" changed.
You can also read and write in one expression by computing the new value from the old one.
Right-hand side first: cart["Pen"] reads 2, the expression becomes 2 + 3, and 5 is written back under "Pen". This works even if the key doesn't exist yet, because the read returns the zero value.
cart["Pen"] on the right-hand side reads the zero value 0 because the key isn't there yet. The expression becomes 0 + 3, and 3 gets written, which also creates the entry. This is the foundation of the counting pattern that closes the lesson.
There's one syntactic shortcut worth knowing. The += operator works on map values directly, so the read-modify-write can be written more compactly.
cart["Pen"] += 3 reads the current value (2), adds 3, and writes back 5. cart["Pencil"] += 1 reads the zero value (0, because the key isn't there), adds 1, and writes 1, which also creates the entry. Same semantics, fewer characters.
To remove an entry, call the delete built-in: delete(m, key). It takes the map and the key, removes that entry, and returns nothing.
The "Pen" entry is gone, the length is now 2, and any future read of stock["Pen"] will return the zero value, just like for any other missing key.
delete is a no-op when the key isn't in the map. No error, no panic, no warning. The function just returns.
Three delete calls on a missing key, three no-ops. The map is untouched. This is deliberate: you don't have to check for membership before deleting, which keeps cleanup code short.
Cost: delete is O(1) on average, but it doesn't shrink the underlying hash table. The buckets that held the deleted entries are marked free and reused for future inserts. A map that grew to a million entries and then had most of them deleted still holds onto roughly the same amount of memory. If you need to release that memory, build a new map with only the entries you want to keep and let the old one be garbage collected.
There's one place where delete does behave differently from a read: a nil map. delete on a nil map is also a no-op, not a panic, which we'll cover in the next section alongside the rest of the nil-map behavior.
lenThe len built-in reports how many key-value pairs the map currently holds. It works on any map, including one that's nil.
len tracks the live entry count. It's O(1) because Go stores the count directly in the map header rather than scanning the buckets.
One caveat: len counts only entries that are currently present. It doesn't tell you the underlying hash table's capacity. There's no cap() for maps the way there is for slices, because the table's internal size isn't part of the language's contract.
A nil map and an empty map both have length 0. From len alone, you can't tell which is which, just like with nil slices and empty slices. The difference shows up when you try to write to them, which is the next section.
A map declared with var m map[K]V is nil until you assign a real map to it. The operations behave very specifically on a nil map, and the rules are worth pinning down.
The summary is in this table:
| Operation | On a nil map | Notes |
|---|---|---|
Read v := m[key] | OK, returns zero value | Behaves like a missing key |
len(m) | OK, returns 0 | No panic |
delete(m, key) | OK, no-op | Documented behavior since Go 1 |
Write m[key] = v | Panics | assignment to entry in nil map |
Three of the four operations work without complaint. Only writing panics. This shows up most often when someone declares a map with var and then forgets to initialize it before assigning to it.
Three safe operations, three sensible outcomes. The map is nil, so it acts like an empty map for any operation that doesn't try to add data.
Writing is the one that fails.
What's wrong with this code?
The var declaration produces a nil map. There's no underlying hash table to store the entry in, and Go doesn't lazily allocate one for you. The write panics at runtime:
Fix:
make allocates the hash table, and the write has somewhere to go. Any of the three forms (literal, make, or assigning a non-nil map) is fine; what matters is that the map isn't nil before you try to write to it.
The asymmetry between read and write on a nil map is deliberate. Reads are pure: they can answer "no such key" without changing anything, so Go can return the zero value safely. Writes have to mutate the hash table, and there's no table to mutate on a nil map. Rather than auto-allocate one (which would surprise callers who expected an explicit make), Go panics. This matches the behavior of nil slices for append (which works because append returns a new slice) versus indexed assignment on a nil slice (which panics, because there's no backing array to index into).
Cost: The panic itself isn't a performance problem, but it crashes your program. Check that a map is initialized before you write to it, especially when the map is a field on a struct that callers might construct with a zero value.
Tallying how often each value shows up is one of the most common uses of a map. The pattern relies on two map behaviors working together: reading a missing key returns the zero value, and writes either insert or update depending on whether the key is already there.
The whole pattern fits on one line inside the loop. counts[item]++ reads the current count (zero if the key isn't there yet), adds one, and writes the result back. The first time a key shows up, the read returns 0 and the write stores 1, creating the entry. Every subsequent appearance increments the existing count.
The same pattern works for any additive tally. Counting how many of each product appeared across many orders looks the same.
Two nested loops, one map, one ++. The pattern scales without changing shape.
The same idea handles sums and not just counts. Total spend per customer falls out by replacing ++ with += amount.
For a new customer, totals[p.customer] reads 0.0, the addition produces the first amount, and the write creates the entry. For repeat customers, the running total grows. The print order isn't fixed because of how map iteration works, but the numbers are stable.
Some updates depend on what's already there. Capping the quantity in a cart at 10 means reading the existing value, deciding whether to keep it or trim it, and writing back.
Pen had 15, which is over the cap, so the write trims it to 10. Notebook and Eraser are below the cap and stay as they were. The loop reads each value, checks a condition, and writes only when the condition matches.
Modifying the value through the loop variable doesn't work in Go. The loop variable qty is a copy of the map value, not a reference into the map. Assigning to qty would change the local copy and leave the map untouched. The fix is to write through the map: cart[item] = newValue. This is safe to do during iteration as long as you only update keys that already exist; adding or deleting keys during iteration has its own rules.
The pattern also handles "increment by a custom amount" cases cleanly. Restocking inventory from a delivery shipment is just a read-add-write per item.
For "Eraser", the read returns 0 (not in stock), the addition produces 75, and the write creates the entry. For "Notebook" and "Pen", the addition runs on the existing values. One loop, one read, one write per item, and the existence check is implicit in the zero-value read.
The same approach works for "first seen wins" updates: only write when there isn't already an entry. Without the comma-ok idiom, the cleanest way to express this is to compare the read to the zero value, with the caveat that this only works when the zero value isn't a legal stored value.
Each product keeps the first price seen because subsequent reads of the same key return the previously stored non-zero value, which fails the == 0 check. This works here because no real price is 0. If 0 were a legal value, you'd need the comma-ok idiom to tell "missing key" from "stored zero," which is exactly the problem the next chapter solves.
This diagram pulls the four CRUD operations together so the surface area of map operations is in one place.
The chain shows the natural lifecycle of a map entry, ending with len for bookkeeping and the nil map rule that sits behind all of it. Most of the operations are forgiving (no errors, no panics, sensible defaults). The one exception, writing to a nil map, is the bug to watch for.
m[key] = value. The map figures out which is happening based on whether the key already exists.counts[item]++ work cleanly but also makes a returned zero ambiguous.delete(m, key) removes an entry, returns nothing, and is a no-op on a missing key or a nil map. It does not release the underlying hash table memory.len(m) reports the current entry count in O(1). It works on a nil map and returns 0.nil map supports read, len, and delete, but writing panics with assignment to entry in nil map. Use make or a map literal before the first write.delete is O(1) on average. Occasional growth events for writes cost O(n) but are infrequent.m[k]++) and the lookup-then-update pattern cover most of what you'll do with maps in practice. Both lean on the zero-value-read behavior.A read that returns zero might mean "missing key" or "stored zero." That ambiguity is the problem the next chapter takes on, with the comma-ok idiom that gives you both the value and a boolean signaling whether the key was actually there.