Skip to main content
Go beginner Lesson 10 of 25

Maps in Go

Create and use Go maps with literals and make, delete keys, check for key existence, and understand iteration order.

Creating Maps

A map is Go’s built-in hash table — it stores key-value pairs and provides average O(1) lookup, insertion, and deletion. Maps are reference types: assigning a map to a new variable does not copy the data, both variables point at the same underlying hash table. Use a map literal when you know the initial entries; use make when you are building the map incrementally and want to pre-size it to avoid rehashing.

// Map literal — creates and populates in one step
scores := map[string]int{
    "Alice":   95,
    "Bob":     87,
    "Charlie": 92,
}

// Empty map literal — ready to use, no entries yet
empty := map[string]int{}

// make with a size hint — tells Go to pre-allocate for ~10 entries
// The hint is advisory; the map will still grow beyond it if needed
m := make(map[string]string, 10)

Reading and Writing

Reading from a map always succeeds — if the key is absent, Go returns the zero value for the value type. This is convenient but can hide bugs when 0 or "" is also a valid stored value. The two-value form of a map lookup resolves this ambiguity: the second value is a boolean that is true only if the key was actually present.

scores := map[string]int{"Alice": 95}

// Write — assign to a key
scores["Bob"] = 87
scores["Charlie"] = 92

// Read — returns zero value if key is absent, no error or panic
fmt.Println(scores["Alice"])   // 95
fmt.Println(scores["Unknown"]) // 0 — zero value, key was not present

// Two-value lookup — distinguishes a missing key from a key with value 0
val, ok := scores["Alice"]
fmt.Println(val, ok) // 95 true

val, ok = scores["Dave"]
fmt.Println(val, ok) // 0 false

// Idiomatic pattern — check and use in one statement, keeps scope tight
if score, ok := scores["Eve"]; ok {
    fmt.Printf("Eve scored %d\n", score)
} else {
    fmt.Println("Eve not found")
}

Deleting Keys

The built-in delete function removes a key from a map. It is always safe to call — deleting a key that does not exist is a no-op, not an error. After deletion, reading the key returns the zero value again.

scores := map[string]int{"Alice": 95, "Bob": 87}

delete(scores, "Bob")
fmt.Println(scores) // map[Alice:95]

// Deleting a non-existent key is a no-op — no error, no panic
delete(scores, "Nobody")

Iterating Over Maps

range over a map yields each key-value pair. The critical thing to know is that iteration order is intentionally random in Go — it changes between runs and even between iterations of the same program. If you need a deterministic output, sort the keys first and then iterate in sorted order.

scores := map[string]int{"Alice": 95, "Bob": 87, "Charlie": 92}

// Iteration order is random — do not rely on it
for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

// Keys only — use when you don't need the values
for name := range scores {
    fmt.Println(name)
}

// Deterministic iteration — collect and sort keys first
import "sort"

names := make([]string, 0, len(scores))
for name := range scores {
    names = append(names, name)
}
sort.Strings(names)

for _, name := range names {
    fmt.Printf("%s: %d\n", name, scores[name])
}
// Alice: 95
// Bob: 87
// Charlie: 92

Maps of Slices

A map whose values are slices is a common and powerful pattern for grouping data. Because reading a missing key returns a nil slice, and append on a nil slice works correctly, you can accumulate groups without checking whether the key already exists.

// Group people by department — append handles the nil slice case automatically
type Employee struct {
    Name       string
    Department string
}

employees := []Employee{
    {"Alice", "Engineering"},
    {"Bob", "Marketing"},
    {"Charlie", "Engineering"},
    {"Diana", "Marketing"},
    {"Eve", "Engineering"},
}

byDept := make(map[string][]Employee)
for _, e := range employees {
    // If the key doesn't exist yet, byDept[e.Department] returns nil,
    // and append on nil creates a new slice — no explicit initialization needed
    byDept[e.Department] = append(byDept[e.Department], e)
}

for dept, people := range byDept {
    fmt.Printf("%s: ", dept)
    for _, p := range people {
        fmt.Printf("%s ", p.Name)
    }
    fmt.Println()
}
// Engineering: Alice Charlie Eve
// Marketing: Bob Diana

Maps of Maps (Nested Maps)

Nested maps are useful for representing hierarchical data like graphs, multi-level configurations, or pivot tables. The main gotcha is that the inner map is not automatically initialized — you must check whether the outer key exists before writing to the inner map.

// Adjacency list representation for a weighted graph
graph := map[string]map[string]int{
    "A": {"B": 1, "C": 4},
    "B": {"C": 2, "D": 5},
    "C": {"D": 1},
}

// Safe access to nested maps — check the outer key first
if neighbors, ok := graph["A"]; ok {
    fmt.Println(neighbors["B"]) // 1
}

// Adding to a nested map safely — initialize the inner map if needed
addEdge := func(g map[string]map[string]int, from, to string, weight int) {
    if _, ok := g[from]; !ok {
        g[from] = make(map[string]int) // must initialize before writing
    }
    g[from][to] = weight
}
addEdge(graph, "D", "A", 3)

Counting with Maps

Using a map to count occurrences of values is one of the most common map patterns. It works cleanly because reading a missing key returns 0, so you can increment without initializing.

// Word frequency counter — missing keys default to 0, so ++ works immediately
text := "the quick brown fox jumps over the lazy dog the"
words := strings.Fields(text)

freq := make(map[string]int)
for _, w := range words {
    freq[w]++ // safe — freq[w] is 0 if w has not been seen before
}

fmt.Println(freq["the"])   // 3
fmt.Println(freq["quick"]) // 1

Set Simulation

Go has no built-in set type, but map[T]struct{} is the idiomatic substitute. Using struct{} as the value type is deliberate — an empty struct occupies zero bytes, so the map only pays for the keys, not for any wasted value storage. This is more efficient than map[T]bool.

seen := make(map[string]struct{})

add := func(s string) { seen[s] = struct{}{} }
has := func(s string) bool { _, ok := seen[s]; return ok }

add("apple")
add("banana")
add("apple") // duplicate — the map simply overwrites the same key, no effect

fmt.Println(has("apple"))  // true
fmt.Println(has("cherry")) // false
fmt.Println(len(seen))     // 2

// Practical use: remove duplicates from a slice while preserving first-seen order
func unique(items []string) []string {
    seen := make(map[string]struct{}, len(items))
    result := make([]string, 0, len(items))
    for _, item := range items {
        if _, ok := seen[item]; !ok {
            seen[item] = struct{}{}
            result = append(result, item)
        }
    }
    return result
}

Concurrency-Safe Maps

A regular Go map is not safe for concurrent use — simultaneous reads and writes from multiple goroutines cause a runtime panic. The two standard solutions are a mutex-protected map (more flexible, works well for any access pattern) and sync.Map (optimized for high read-to-write ratios with many goroutines reading the same keys).

import "sync"

// Option 1: sync.RWMutex wrapping a regular map — use this for most cases
type SafeMap struct {
    mu sync.RWMutex
    m  map[string]int
}

func (sm *SafeMap) Set(key string, val int) {
    sm.mu.Lock()         // exclusive lock for writes
    defer sm.mu.Unlock()
    sm.m[key] = val
}

func (sm *SafeMap) Get(key string) (int, bool) {
    sm.mu.RLock()        // shared lock — multiple readers can proceed simultaneously
    defer sm.mu.RUnlock()
    v, ok := sm.m[key]
    return v, ok
}

// Option 2: sync.Map — best for high-read, low-write scenarios with many goroutines
var sm sync.Map

sm.Store("key", 42)
val, ok := sm.Load("key")
sm.Delete("key")
sm.Range(func(k, v any) bool {
    fmt.Println(k, v)
    return true // return false to stop iteration early
})

Practical Example — Inventory System

This example shows maps working together in a realistic domain model. Two parallel maps track quantity and price for the same set of keys, demonstrating how maps compose to represent structured data before you introduce a struct.

package main

import (
    "fmt"
    "sort"
)

type Inventory struct {
    stock map[string]int
    price map[string]float64
}

func NewInventory() *Inventory {
    return &Inventory{
        stock: make(map[string]int),
        price: make(map[string]float64),
    }
}

func (inv *Inventory) AddItem(name string, qty int, unitPrice float64) {
    inv.stock[name] += qty      // safe: zero value for missing key is 0
    inv.price[name] = unitPrice
}

func (inv *Inventory) Sell(name string, qty int) error {
    if inv.stock[name] < qty {
        return fmt.Errorf("insufficient stock: have %d, need %d", inv.stock[name], qty)
    }
    inv.stock[name] -= qty
    if inv.stock[name] == 0 {
        // Clean up both maps when stock reaches zero
        delete(inv.stock, name)
        delete(inv.price, name)
    }
    return nil
}

func (inv *Inventory) Report() {
    // Sort keys for deterministic output
    items := make([]string, 0, len(inv.stock))
    for name := range inv.stock {
        items = append(items, name)
    }
    sort.Strings(items)

    fmt.Printf("%-15s %5s %8s\n", "Item", "Qty", "Value")
    fmt.Println("-----------------------------")
    for _, name := range items {
        qty := inv.stock[name]
        value := float64(qty) * inv.price[name]
        fmt.Printf("%-15s %5d %8.2f\n", name, qty, value)
    }
}

func main() {
    inv := NewInventory()
    inv.AddItem("Widget", 100, 2.50)
    inv.AddItem("Gadget", 50, 15.00)
    inv.AddItem("Doohickey", 200, 0.75)

    inv.Sell("Widget", 30)
    inv.Sell("Gadget", 50) // sells all — removes from both maps

    inv.Report()
}

Output:

Item              Qty    Value
-----------------------------
Doohickey         200   150.00
Widget             70   175.00

Frequently Asked Questions

What happens when you read a key that doesn't exist in a map?
Go returns the zero value for the map's value type — 0 for int, empty string for string, nil for pointers and slices. To distinguish between a missing key and a key with the zero value, use the two-value lookup: val, ok := m[key].
Are Go maps safe for concurrent use?
No. Concurrent reads are safe, but concurrent reads and writes (or concurrent writes) cause a runtime panic. Use sync.RWMutex to protect a map, or use sync.Map for high-concurrency scenarios.
Is map iteration order guaranteed in Go?
No. The Go spec explicitly says map iteration order is random and varies between runs. This is intentional — it prevents code from accidentally depending on insertion order.