Maps: sets & nested maps
Reviewed & published by Brayan K
Maps are Go's unordered key/value tables, and with a few patterns — the empty-struct set, the comma-ok check, sorted-key printing, and nested maps — you can model membership, deduplication, and grouped data cleanly.
Part of the free Go course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
- Build a set with map[T]struct{}
- Use comma-ok to test key presence
- Safely delete entries while ranging
- Print maps deterministically by sorting keys
- Create and read nested maps
- Use the maps package (Go 1.21+)
1️⃣ Sets with map[T]struct{}
Go has no set type, so you use a map. The idiomatic set is map[T]struct{}{} — the empty struct value occupies zero bytes, so only the keys matter. Add with seen[x] = struct{}{}, and test membership with the comma-ok form so you don't accidentally insert. This is the standard way to deduplicate or track "have I seen this before?".
package main
import "fmt"
func main() {
// A set is a map whose value type is the empty struct{} — zero bytes.
seen := map[string]struct{}{}
for _, w := range []string{"go", "rust", "go", "c", "rust"} {
seen[w] = struct{}{} // add to the set
}
fmt.Println("unique count:", len(seen))
// comma-ok: check membership without inserting.
_, ok := seen["go"]
fmt.Println("has go? ", ok) // true
_, ok = seen["java"]
fmt.Println("has java?", ok) // false
}2️⃣ Unordered Iteration & Deleting While Ranging
Map iteration order is deliberately randomized, so never depend on it. To produce deterministic output, collect the keys into a slice, sort them, and range the sorted keys. The good news: it is safe to delete the current (or already-seen) entry while ranging the same map — a common way to filter in place.
package main
import (
"fmt"
"sort"
)
func main() {
scores := map[string]int{"ada": 90, "ben": 75, "cara": 88}
// Map iteration order is RANDOM. To print deterministically,
// collect the keys, sort them, then range the sorted keys.
keys := make([]string, 0, len(scores))
for k := range scores {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
fmt.Printf("%s=%d\n", k, scores[k])
}
// It's SAFE to delete during a range over the same map.
for k, v := range scores {
if v < 80 {
delete(scores, k)
}
}
fmt.Println("remaining:", len(scores)) // ben removed -> 2
}3️⃣ Nested Maps & the maps Package
A nested map (map[string]map[string]int) groups data two levels deep. The catch: an inner map starts as nil, and writing to a nil map panics — so initialize the inner map before assigning into it. Since Go 1.21 the maps package adds helpers like maps.Keys (an iterator), which pairs nicely with slices.Sorted to get sorted keys in one line.
package main
import (
"fmt"
"maps"
"slices"
)
func main() {
// Nested map: outer key -> inner map. Initialize the inner map
// before writing into it, or you'll panic on a nil map.
grades := map[string]map[string]int{}
add := func(student, subject string, score int) {
if grades[student] == nil {
grades[student] = map[string]int{}
}
grades[student][subject] = score
}
add("ada", "math", 95)
add("ada", "art", 80)
add("ben", "math", 70)
fmt.Println("ada/math:", grades["ada"]["math"]) // 95
// The maps package (Go 1.21+): Keys returns an iterator;
// slices.Sorted collects it into a sorted slice.
students := slices.Sorted(maps.Keys(grades))
fmt.Println("students:", students) // [ada ben]
}🎯 Your Turn
Use comma-ok to distinguish "stored value is 0" from "key is missing". Fill in the two blanks.
package main
import "fmt"
func main() {
inventory := map[string]int{"apples": 3, "pears": 0}
// Use comma-ok to tell "value is 0" apart from "key is missing".
count, ___ := inventory["pears"] // 👉 ok
fmt.Println("pears:", count, "present:", ok)
_, ok2 := inventory["___"] // 👉 kiwis (a missing key)
fmt.Println("kiwis present:", ok2)
// ✅ Expected output:
// pears: 0 present: true
// kiwis present: false
}Common Errors (and the fix)
- ❌ "panic: assignment to entry in nil map" — you wrote into a map you never created. ✅ m := map[K]V{} or make(map[K]V) first; for nested maps, init the inner map too.
- ❌ Can't tell "missing" from "zero" — a plain lookup returns the zero value. ✅ Use v, ok := m[k].
- ❌ Output order changes every run — map ranging is randomized. ✅ Sort the keys before printing.
- ❌ Adding keys while ranging gives flaky results — that's undefined. ✅ Collect changes and apply after, or build a new map.
🎯 Mini-Challenge: Word Frequency
Count how often each word appears, then print the words in sorted order with their counts. Split with strings.Fields, tally with counts[w]++, and sort the keys before printing.
package main
import (
"fmt"
"sort"
"strings"
)
func main() {
// 🎯 MINI-CHALLENGE: count words, print in sorted order.
// 1) strings.Fields(text) splits on whitespace.
// 2) Increment counts[word] for each word (zero value is 0, so ++ works).
// 3) Collect the keys, sort.Strings them, and print "word: count".
//
// ✅ Expected output:
// cat: 2
// mat: 1
// on: 1
// sat: 1
// the: 3
text := "the cat sat on the mat the cat"
_ = sort.Strings
_ = strings.Fields
// your code here
}🎉 Lesson Complete!
- ✅ A set is map[T]struct{} — keys only, zero-byte values
- ✅ v, ok := m[k] distinguishes a missing key from a zero value
- ✅ delete during range is safe; adding during range is not
- ✅ Map order is random — sort the keys for deterministic output
- ✅ Initialize inner maps before writing; maps.Keys + slices.Sorted help
Practice quiz
How do you model a set of strings idiomatically in Go?
- map[string]int
- map[string]struct{}
- []string only
- map[int]string
Answer: map[string]struct{}. map[string]struct{} stores keys with zero-byte values, the idiomatic Go set.
Which form distinguishes a missing key from a stored zero value?
- v := m[k]
- v, ok := m[k]
- len(m[k])
- m.Has(k)
Answer: v, ok := m[k]. The comma-ok form v, ok := m[k] returns ok=false when the key is absent.
What happens to iteration order when you range over a map?
- Sorted ascending
- Insertion order
- Random / unspecified
- Sorted descending
Answer: Random / unspecified. Go deliberately randomizes map iteration order; sort the keys for deterministic output.
Is deleting the current key safe while ranging over a map?
- No, it panics
- Yes, it is safe
- Only with a lock
- Only for string keys
Answer: Yes, it is safe. Deleting entries during a range is explicitly safe in Go.
Is adding new keys safe while ranging over a map?
- Always safe
- Not guaranteed — they may or may not be visited
- Panics immediately
- Adds them to the front
Answer: Not guaranteed — they may or may not be visited. Adding keys during iteration is unspecified — added entries may or may not appear.
What does writing to a nil map do?
- Creates the map
- Panics at runtime
- Silently ignores
- Returns an error
Answer: Panics at runtime. Writing to a nil (uninitialized) map panics; you must make/initialize it first.
Before writing into an inner map of a nested map, you must:
- Nothing special
- Initialize that inner map first
- Lock the outer map
- Sort the keys
Answer: Initialize that inner map first. Each inner map is nil until created, so initialize it before assigning into it.
Which combination gives sorted keys from a map in modern Go?
- maps.Keys + slices.Sorted
- sort.Map
- map.Sort()
- keys.Order()
Answer: maps.Keys + slices.Sorted. maps.Keys yields the keys and slices.Sorted collects them in sorted order.
What does delete(m, k) do when k is not in the map?
- Panics
- Adds k
- Does nothing
- Returns an error
Answer: Does nothing. delete is a no-op when the key is absent — no panic, no error.
Reading a missing nested key like m["a"]["b"] when m["a"] exists returns:
- A panic
- The value's zero value
- An error
- nil only
Answer: The value's zero value. Reading a missing key returns the value type's zero value, so it is safe.
Continue this course
- Previous: Slice Internals: append, copy, capacity
- Next: Sorting (sort & slices.Sort) — sort.Slice with a less func, stable sorts, and sorting structs by field
Frequently asked questions
How do I make a set in Go?
Go has no built-in set type, so you use a map. The idiomatic set is map[T]struct{}{}: the empty struct value takes zero bytes, so the map only stores keys. Add with seen[x] = struct{}{}, test membership with the comma-ok form _, ok := seen[x], and remove with delete(seen, x). A simpler-to-read alternative is map[T]bool, at the cost of one byte per entry.
What is the comma-ok pattern for maps?
Indexing a map for a missing key returns the value type's zero value, so you can't tell 'present with value 0' from 'absent'. The two-result form value, ok := m[key] solves this: ok is true only when the key actually exists. Use it whenever a zero value is a legitimate stored value, or whenever you need to branch on presence.
Is it safe to delete from a map while ranging over it?
Yes. The Go spec explicitly allows deleting the current or already-seen entries during a range over the same map. What's not guaranteed is whether entries you ADD during iteration will be visited — they may or may not appear. So delete-while-ranging is fine; add-while-ranging is unpredictable.
Why is my map printed in a different order every run?
Map iteration order in Go is intentionally randomized so you don't accidentally depend on it. To get deterministic output, collect the keys into a slice, sort that slice (sort.Strings, sort.Ints, or slices.Sort), and then range over the sorted keys to read the values in order.
How do I build a nested map without panicking?
A map value that hasn't been created is nil, and writing to a nil map panics. For map[string]map[string]int you must initialize each inner map before assigning into it: if m[outer] == nil { m[outer] = map[string]int{} }. Reading a missing nested key is fine (you get zero values), but writing requires the inner map to exist first.