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

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)

🎯 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!

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

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.