Iterating over maps with range
You're building a word counter for a chat bot. You've stored the results in a map where each word maps to its frequency. Now you need to find the most common word, or print the results to a log, or filter out words that appeared fewer than three times. You can't just grab the whole map and dump it. You need to walk through every entry, inspect the key and value, and decide what to do.
Go provides the range keyword for this. It yields key-value pairs one by one. The first variable gets the key, the second gets the value. Maps are unordered, so the pairs arrive in a random sequence. That randomness is a feature, not a bug. It prevents code from accidentally depending on iteration order.
Think of a map like a bag of marbles. You reach in and pull out a pair: the key and the value. You can't predict which pair comes out next. The bag doesn't care about order. It just gives you everything eventually. If you need order, you have to take the marbles out, sort them, and put them in a line. That's a slice's job.
The basic loop
Here's the standard loop: define a map, run range, print each pair.
package main
import "fmt"
func main() {
// Map stores word counts. Keys are strings, values are ints.
counts := map[string]int{
"hello": 5,
"world": 3,
"go": 12,
}
// Range yields key and value for each entry.
// Order is random; don't rely on it.
for word, count := range counts {
fmt.Printf("%s: %d\n", word, count)
}
}
The compiler generates code to iterate over the map's internal hash table. range walks the buckets and produces pairs. It does not copy the map. It reads the current state. If the map is empty, the loop body never runs. If the map is nil, the loop body also never runs. Ranging over a nil map is safe. It behaves exactly like ranging over an empty map.
If you only need the keys, use the blank identifier _ for the value. This tells the compiler you intentionally discarded the value. It's a convention to show you thought about the return value and chose to ignore it.
// Collect all keys into a slice.
// The blank identifier discards the value.
keys := make([]string, 0, len(counts))
for word := range counts {
keys = append(keys, word)
}
Maps are unordered. If you need deterministic output, extract the keys into a slice and sort the slice. The map itself never guarantees order.
Copy semantics and mutation
Range yields copies of the values. This is the most common trap for newcomers. If the map stores structs, the loop variable is a copy of the struct. Modifying the loop variable does not modify the map.
Here's a function that tries to update a map entry inside the loop. It fails silently because the assignment targets the local copy.
type Item struct {
ID int
Name string
}
func updateNames(items map[int]Item) {
// Loop variable item is a copy of the map value.
// Changing item.Name does not affect items[id].Name.
for id, item := range items {
item.Name = "Updated"
// The map remains unchanged.
}
}
To modify a value, assign back to the map using the key. The key is also a copy, but keys are usually immutable primitives like strings or ints. You use the key to index the map and write the new value.
func updateNamesCorrect(items map[int]Item) {
for id, item := range items {
// Modify the copy.
item.Name = "Updated"
// Write the copy back to the map.
items[id] = item
}
}
If the map stores pointers, the loop variable is a copy of the pointer. The pointer points to the same heap object. Modifying the pointed-to struct works without writing back.
func updateNamesPointer(items map[int]*Item) {
// item is a copy of the pointer, but it points to the same struct.
// Modifying item.Name updates the struct on the heap.
for _, item := range items {
item.Name = "Updated"
}
}
Maps are not thread-safe. If one goroutine ranges over a map while another goroutine writes to it, the program may panic with a concurrent map read and map write error. Use a mutex or channels to protect the map. Concurrency bugs are hard to reproduce. Protect shared maps from the start.
Key constraints and compiler errors
Map keys must be comparable. Go checks this at compile time. Strings, integers, floats, booleans, and structs with comparable fields are valid keys. Slices, maps, and functions are not comparable. They cannot be keys.
If you try to use a slice as a key, the compiler rejects the program with invalid map key type []int. The error message names the type that caused the problem. Fix it by using a comparable type, or convert the slice to a string or hash.
// This compiles. Struct fields are comparable.
type Point struct {
X int
Y int
}
m := map[Point]string{}
// This fails. Slices are not comparable.
// Compiler error: invalid map key type []int
// badMap := map[[]int]string{}
If you try to range over a value that isn't iterable, the compiler complains with cannot range over x (variable of type int). Only arrays, slices, strings, maps, and channels support range.
Realistic example: filtering and transforming
Here's a function that scans a map to find entries that meet a threshold and builds a new map with only those entries. This pattern appears often when cleaning data or preparing payloads.
// FilterCounts returns a new map containing only entries with count >= min.
func FilterCounts(counts map[string]int, min int) map[string]int {
// Pre-allocate result map.
// Capacity is an estimate; the map may grow.
result := make(map[string]int, len(counts))
for word, count := range counts {
// Include entry only if count meets threshold.
if count >= min {
result[word] = count
}
}
return result
}
The function returns a new map. It does not modify the input. This is safer. Callers can keep the original data. The convention "accept interfaces, return structs" applies here too. Functions that take a map usually accept the concrete map type because maps are reference-like already. You don't need to wrap a map in an interface just to pass it around.
Pitfalls and runtime behavior
Maps are unordered. If your code depends on order, it will break when the map grows or when you run on a different Go version. The iteration order can change between runs. Extract keys to a slice and sort if you need order.
You can add or delete keys while ranging over a map. The loop might see the new key, or it might not. It won't panic. But don't rely on seeing every change. The behavior is defined but unpredictable. If you need to remove entries based on a condition, collect the keys to delete and remove them after the loop.
// Collect keys to delete.
// Deleting during iteration is safe but order is unpredictable.
var toDelete []string
for word, count := range counts {
if count == 0 {
toDelete = append(toDelete, word)
}
}
// Delete after iteration.
for _, word := range toDelete {
delete(counts, word)
}
Ranging over a nil map is safe. The loop body never runs. This makes nil checks optional in many cases. You can range over a map that might be nil without panicking. Just be aware that the loop won't execute.
The len function returns the number of entries in O(1) time. It does not iterate. Use len(m) to check size before ranging if you need to skip empty maps.
Maps are cheap to create. make(map[string]int) allocates a small hash table. The cost is negligible. Don't hoard maps across requests. Create a new map per request or per logical unit of work. Long-lived maps can grow unbounded if you never delete keys. Memory leaks happen when maps accumulate entries forever. Delete keys you no longer need.
Decision matrix
Use range over a map when you need to process every key-value pair and order doesn't matter. Use range with a blank identifier when you only need keys or only need values. Use a slice of keys plus sort when you need deterministic output order. Use a single m[key] lookup when you only need one specific value. Use delete(m, key) inside a loop when you need to remove entries, accepting that iteration order remains random. Use a mutex when multiple goroutines access the same map.
Maps are unordered. If you need order, you need a slice.