A map is Go’s built-in key-value store (called a “dictionary” or “hash” in other languages). Most map operations are built into the language — you don’t need a package for them. But the standard maps package (Go 1.21+) adds a few extras: cloning, comparing, filtering.

This page covers both the built-in map operations and the maps package, end to end.

import "maps"   // only needed for the package functions below

At a glance

Need to…How
Create a mapmake(map[K]V) or map[K]V{}
Insert / updatem[key] = value
Look upv := m[key] (returns zero value if missing)
Check if key existsv, ok := m[key]
Deletedelete(m, key)
Lengthlen(m)
Iteratefor k, v := range m (order is random!)
Copy a mapmaps.Clone(m)
Compare two mapsmaps.Equal(a, b)
Filter in placemaps.DeleteFunc(m, func)
Get all keys / valuesmaps.Keys(m) / maps.Values(m)

Built-in operations

Creating a map

// Make an empty map
ages := make(map[string]int)

// Make with a capacity hint (faster for big maps)
ages := make(map[string]int, 1000)

// Make with values
ages := map[string]int{
    "Alice": 30,
    "Bob":   22,
    "Carol": 28,
}

// Nil map — declared but not initialized
var ages map[string]int
// Reading from a nil map is fine (returns zero value)
// Writing to a nil map PANICS

The capacity hint isn’t a limit — it’s a starting size. Maps grow automatically. The hint just avoids early reallocations.

Insert, update, delete

ages["Dave"] = 40           // insert
ages["Alice"] = 31          // update (same syntax)

delete(ages, "Bob")         // remove; no-op if key doesn't exist

Lookup — and the comma-ok idiom

A plain lookup returns the zero value if the key is missing, with no way to tell “missing” from “present with zero value”:

age := ages["Eve"]          // 0 if Eve isn't there — but also 0 if Eve is 0 years old

The comma-ok form distinguishes them:

age, ok := ages["Eve"]
if !ok {
    fmt.Println("Eve isn't in the map")
} else {
    fmt.Println("Eve's age:", age)
}

This is one of the most-asked Go idioms in interviews. Memorize the shape.

Iteration — and the random-order gotcha

for name, age := range ages {
    fmt.Println(name, age)
}

Map iteration order is randomized by design. Each range may visit keys in a different order, even within the same run. If you need a stable order, sort the keys yourself:

keys := make([]string, 0, len(ages))
for k := range ages {
    keys = append(keys, k)
}
slices.Sort(keys)
for _, k := range keys {
    fmt.Println(k, ages[k])
}

Deleting during iteration is safe

Unlike some languages, you can delete from a map while iterating over it without breaking the loop:

for name, age := range ages {
    if age < 18 {
        delete(ages, name)  // safe
    }
}

Adding during iteration is unsafe — the new key may or may not be visited; behavior is undefined. Don’t.

The maps package (Go 1.21+)

maps.Clone — independent copy

a := map[string]int{"x": 1, "y": 2}
b := maps.Clone(a)
b["x"] = 99
// a["x"] is still 1

b := a does NOT copy. Both variables point to the same underlying map. maps.Clone gives you a real copy.

maps.Copy — merge into an existing map

Copies all entries from src into dst, overwriting on collision.

dst := map[string]int{"a": 1}
src := map[string]int{"b": 2, "a": 99}

maps.Copy(dst, src)
// dst = {"a": 99, "b": 2}

maps.Equal — element-by-element equality

a := map[string]int{"x": 1, "y": 2}
b := map[string]int{"y": 2, "x": 1}   // same content, different insert order

maps.Equal(a, b)                       // true

Order doesn’t matter. Two maps are equal if they have the same keys mapping to the same values.

maps.DeleteFunc — filter in place

Removes every entry where the predicate returns true:

maps.DeleteFunc(ages, func(name string, age int) bool {
    return age < 18
})
// removes everyone under 18

maps.Keys and maps.Values — extract as a slice

Note: in Go 1.23+ these return iterators (iter.Seq). In Go 1.21 and 1.22 they returned []K/[]V slices directly. The patterns below cover both.

In Go 1.23+ (iterator form):

import "slices"

keys := slices.Collect(maps.Keys(ages))     // []string
values := slices.Collect(maps.Values(ages)) // []int

In Go 1.21 / 1.22 (slice form):

keys := maps.Keys(ages)     // []string
values := maps.Values(ages) // []int

Either way, the order is random — slice the keys and sort them if you need a stable order.

Common interview patterns

Map as a set

Go has no built-in set type. The idiomatic substitute is map[T]struct{}struct{} takes zero memory.

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

seen["apple"] = struct{}{}
seen["banana"] = struct{}{}

_, exists := seen["apple"]   // true
_, exists = seen["cherry"]   // false

delete(seen, "apple")

Using map[string]bool works too and is sometimes clearer. The struct{} version is just slightly more memory-efficient.

Count occurrences

words := []string{"go", "rust", "go", "python", "go", "rust"}

counts := make(map[string]int)
for _, w := range words {
    counts[w]++
}
// counts = {"go": 3, "rust": 2, "python": 1}

This works because reading a missing key returns the zero value (0 for int), and counts[w]++ is shorthand for counts[w] = counts[w] + 1.

Find the most frequent value

var top string
maxCount := 0
for word, count := range counts {
    if count > maxCount {
        top = word
        maxCount = count
    }
}
// top = "go", maxCount = 3

Group items by a key

type Person struct {
    Name string
    City string
}

people := []Person{
    {"Alice", "NYC"},
    {"Bob", "LA"},
    {"Carol", "NYC"},
}

byCity := make(map[string][]Person)
for _, p := range people {
    byCity[p.City] = append(byCity[p.City], p)
}
// byCity = {
//   "NYC": [{Alice NYC} {Carol NYC}],
//   "LA":  [{Bob LA}],
// }

This idiom — appending to a slice stored in a map — works even though byCity[city] is nil on first access, because append accepts a nil slice.

Two-sum (classic interview problem)

Given a slice of integers and a target, find two indices whose values sum to the target:

func twoSum(nums []int, target int) (int, int) {
    seen := make(map[int]int)  // value -> index
    for i, n := range nums {
        if j, ok := seen[target-n]; ok {
            return j, i
        }
        seen[n] = i
    }
    return -1, -1
}

The map turns this from O(n²) (nested loops) into O(n) (one pass). Classic interview answer.

Quick rules of thumb

  • Create with make when you’ll add lots of entries — pass a capacity hint if you know roughly how many.
  • Use comma-ok (v, ok := m[k]) when “missing” matters — plain lookup hides the difference.
  • Map iteration order is random — sort keys if you need stability.
  • Need a set?map[T]struct{}
  • Need to copy a map?maps.Clone, never b := a
  • Writing to a nil map? → Panic. Always make first.

Common mistakes

  • b := a thinking it copies the map — both point to the same map. Use maps.Clone.
  • Writing to a nil map — panics. var m map[string]int then m["x"] = 1 blows up.
  • Relying on iteration order — Go intentionally randomizes it. Tests that depend on order are flaky.
  • if m[key] != 0 to check existence — fails when the value is genuinely zero. Use comma-ok.
  • Using len(m) == 0 to check for nil — works (len is safe on nil maps), but m == nil is clearer if that’s what you actually mean.
  • Iterating over maps.Keys and expecting alphabetical order — random. Always sort if you care.
Toggle theme (T)