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