Back to Notes

Go Maps

Overview

A map is Go's built-in hash map — an unordered collection of key-value pairs with O(1) average lookup, insert, and delete. Keys must be a comparable type (anything you can use == on). Maps are reference types — passing a map to a function gives the function access to the same underlying data.

Python equivalent: dict. JavaScript equivalent: object / Map.


Creating Maps

make — preferred

Always use make (or a literal) before writing. A nil map panics on write.

m := make(map[string]int)       // empty, ready to use
m := make(map[string]int, 100)  // pre-allocated for ~100 entries (hint, not limit)

Map literal

Define and populate in one shot.

ages := map[string]int{
    "Alice": 30,
    "Bob":   25,
    "Carol": 35,
}

// Empty literal — same as make, but less idiomatic
m := map[string]int{}

CRUD Operations

Insert / Update

Same syntax — if the key exists it's overwritten, otherwise it's inserted.

m := make(map[string]int)

m["alice"] = 25   // insert
m["alice"] = 26   // update — no error, just overwrites

Read

Reading a missing key returns the zero value for the value type — not an error. Use the comma-ok idiom to distinguish "key is 0" from "key doesn't exist".

age := m["alice"]     // 26
age  = m["unknown"]   // 0 — zero value, NOT an error

Delete

delete is a no-op if the key doesn't exist — safe to call unconditionally.

delete(m, "alice")    // removes alice
delete(m, "nobody")  // no-op — no panic

Length

fmt.Println(len(m))   // number of key-value pairs

Comma-ok Idiom — Check Key Existence

The two-value form of a map read returns a boolean ok that is true only if the key was present. This is the idiomatic way to tell if a key exists.

m := map[string]int{"alice": 30}

// Two-value form
age, ok := m["alice"]
if ok {
    fmt.Printf("Alice is %d years old\n", age)
} else {
    fmt.Println("alice not found")
}

// Inline — scope ok to the if block
if age, ok := m["bob"]; ok {
    fmt.Println("Bob's age:", age)
}

// Check-only (discard value)
_, exists := m["carol"]
fmt.Println(exists)   // false

Iterating Over Maps

Use range. Order is randomised on every run — this is by design (prevents relying on insertion order).

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

// Key + value
for name, score := range scores {
    fmt.Printf("%s: %d\n", name, score)
}

// Keys only
for name := range scores {
    fmt.Println(name)
}

Sorted iteration

import "sort"

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])
}

Map of Slices

A common pattern — each key maps to a list of values.

// Group people by city
cityPeople := make(map[string][]string)

cityPeople["Mumbai"] = append(cityPeople["Mumbai"], "Alice")
cityPeople["Mumbai"] = append(cityPeople["Mumbai"], "Bob")
cityPeople["Delhi"]  = append(cityPeople["Delhi"], "Carol")

fmt.Println(cityPeople["Mumbai"])  // [Alice Bob]

// Reading a missing key returns nil — append to nil is safe
cityPeople["Pune"] = append(cityPeople["Pune"], "Dave")  // ✅

Map of Maps (Nested Maps)

// Config: section → key → value
config := map[string]map[string]string{
    "database": {
        "host": "localhost",
        "port": "5432",
    },
    "server": {
        "host": "0.0.0.0",
        "port": "8080",
    },
}

fmt.Println(config["database"]["host"])   // localhost

Set Pattern — map[K]struct{}

When you only care about whether a key exists (not its value), use struct{} as the value type — it occupies zero bytes.

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

words := []string{"go", "is", "fast", "go", "is", "great"}
for _, w := range words {
    seen[w] = struct{}{}
}

fmt.Println(len(seen))   // 4 — duplicates removed

// Check membership
_, exists := seen["go"]
fmt.Println(exists)      // true

// Alternatively — bool values are simpler to read
visited := make(map[int]bool)
visited[42] = true
if visited[42] { fmt.Println("been here") }

Nil Map Behaviour

A nil map behaves like an empty map for reads — but panics on write. Always initialise before writing.

var m map[string]int   // nil

// Read — safe, returns zero value
v := m["key"]          // 0, no panic

// Write — PANICS
m["key"] = 1           // panic: assignment to entry in nil map

// Fix
m = make(map[string]int)
m["key"] = 1           // ✅

Maps are Reference Types

When you assign a map to another variable or pass it to a function, both refer to the same underlying data. There's no implicit copy.

a := map[string]int{"x": 1}
b := a          // b and a point to the SAME map

b["x"] = 99
fmt.Println(a["x"])   // 99 — a is also changed

// To get an independent copy, copy manually
c := make(map[string]int, len(a))
for k, v := range a {
    c[k] = v
}

Maps Cheatsheet

OperationCode
Createmake(map[K]V)
Create + initmap[K]V{k: v, ...}
Insert / Updatem[k] = v
Readv := m[k]
Check existencev, ok := m[k]
Deletedelete(m, k)
Lengthlen(m)
Iteratefor k, v := range m {}
Set (key only)map[K]struct{}

Gotchas

GotchaDetail
Write to nil mapPanics — always initialise with make or a literal
Zero value on missing keym["x"] returns 0/"" — not an error. Use comma-ok to distinguish
Iteration orderRandom by design — sort keys if order matters
Maps are reference typesNo implicit copy on assignment or function call
Map key must be comparableSlices, maps, and functions cannot be map keys
Not concurrency-safeConcurrent reads are fine; concurrent read+write is a data race. Use sync.Map or a mutex