Working with Maps

Learn how to read and write entries, use the comma-ok idiom, delete entries, and iterate over maps in Go.

A map in Go is a reference to an unordered collection of key‑value pairs. Once a map has been initialised with make or a literal, the real work begins: adding data, looking it up, checking whether a key is present, removing entries, and walking through the entire collection. The syntax for all of these operations is compact, but it hides a few behaviours that surprise newcomers — especially the zero‑value default, the nil map write panic, and the way iteration order intentionally varies.

This page covers the four core operations you will use every day when working with maps. Each section stands on its own, so you can jump to the one you need right now.

Reading and Writing Entries

Writing a value into a map uses the same assignment syntax as an array or slice: m[key] = value. If the key already exists, its value is silently overwritten. If the key is new, the map grows to hold it. Reading uses the mirror expression: value := m[key].

Never write to a nil map:

A map that has been declared but not initialised has the value nil. Reading from a nil map returns the zero value for the value type, but writing to it panics at runtime. Always initialise a map with make or a literal before you store entries.

package main
import "fmt"
func main() {
    // Initialise with make
    scores := make(map[string]int)
    // Write two entries
    scores["Alice"] = 92
    scores["Bob"] = 85
    // Read one back
    fmt.Println("Alice's score:", scores["Alice"])
    // Overwrite an existing key
    scores["Alice"] = 95
    fmt.Println("Alice's updated score:", scores["Alice"])
    // Read a key that has never been written
    unknown := scores["Charlie"]
    fmt.Println("Charlie's score:", unknown) // prints 0
}

The assignment scores["Charlie"] does not cause an error, even though "Charlie" was never inserted. Instead, reading a missing key returns the zero value for the map’s value type — 0 for int, "" for string, false for bool, and so on. This is one of Go’s most deliberate design choices: you never have to check for null just to read a value. It also means you cannot tell from the value alone whether the key is absent or genuinely stored with the zero value.

Writing to a map is not concurrency‑safe. If multiple goroutines access the same map without synchronisation, the runtime may panic or the map may become corrupted. Use a sync.RWMutex or the sync.Map type when concurrent access is required.

The Comma-Ok Idiom

Because a missing key and a key set to the zero value produce the same result, Go gives you a second boolean return value that answers exactly one question: “was that key in the map?”

value, ok := scores["Charlie"]
if ok {
    fmt.Println("Charlie's score is", value)
} else {
    fmt.Println("Charlie has no score recorded")
}

ok is true when the key exists, regardless of what value it holds. If ok is false, the map really has no entry for that key, and value is the zero value. You can use the underscore to discard the value if all you need is the existence check:

if _, ok := scores["Dana"]; !ok {
    fmt.Println("Dana not found — maybe check the spelling?")
}

Don't ignore the boolean when zero is a valid value:

A map of counters (map[string]int) might contain a legitimate zero count for a key. If you only read m[key] and act on the zero, you could treat a real entry as absent. Always use the comma‑ok idiom when zero can carry meaning.

This idiom is so common in Go that it has its own name. It appears in map lookups, type assertions, and channel receives. The pattern is identical everywhere: two return values, the second being a boolean that tells you whether the operation was successful.

The zero‑value shortcut when you can trust it

There are cases where the zero value alone gives you the right behaviour and a two‑value check would just be noise. For example, a map[string]bool can act as a set:

seen := make(map[string]bool)
words := []string{"go", "map", "go"}
for _, w := range words {
    if seen[w] {
        fmt.Println("Duplicate:", w)
    }
    seen[w] = true
}

Because the zero value of bool is false, seen[w] returns false for every new word. That is exactly the right meaning: “not seen yet”. The comma‑ok idiom adds nothing here, so you can leave it out without losing correctness.

Zero value as a feature:

When the zero value aligns with your default (false for sets, empty slice for groupings), reading a missing key naturally does what you want. This is a deliberate, idiomatic pattern in Go.

Deleting Entries

The built‑in function delete removes a key‑value pair from a map. Its signature is simple:

delete(m, key)

delete does not return a value. If the key is absent, delete is a no‑op — it does nothing and does not panic.

// Remove the entry for "Alice"
delete(scores, "Alice")
// Attempt to delete a key that doesn't exist (perfectly safe)
delete(scores, "Zara")

delete on a nil map panics:

Just like writing, calling delete on a nil map triggers a runtime panic. Always ensure your map is initialised before calling delete.

A common pattern is to check existence before deletion, especially when you want to log or return an error if the key isn’t there:

if _, ok := scores["Eve"]; !ok {
    fmt.Println("Eve not found; nothing to delete")
} else {
    delete(scores, "Eve")
    fmt.Println("Eve removed")
}

There is no built‑in function to clear every entry from a map. To empty a map without creating a new one, you have two options: iterate through all keys and delete each one, or reassign the map variable to a new empty map (old entries become eligible for garbage collection if nothing else references the old map). In Go 1.21 and later, the clear built‑in removes all entries in place:

clear(scores)  // all entries removed, map remains non-nil

Iterating Maps

The for range loop lets you walk over every entry in a map. You can grab the key and value together, just the key, or just the value.

inventory := map[string]int{
    "apples":  5,
    "bananas": 2,
    "oranges": 8,
}
for fruit, quantity := range inventory {
    fmt.Printf("We have %d %s\n", quantity, fruit)
}

If you need only the keys, omit the value:

for fruit := range inventory {
    fmt.Println(fruit)
}

If you need only the values, use the blank identifier for the key:

for _, qty := range inventory {
    fmt.Println("Item count:", qty)
}

Two properties of map iteration surprise newcomers. First, the order is not specified and is intentionally randomised from one run to the next. The runtime even randomises the starting bucket to make the lack of order obvious during development. Second, adding or deleting entries while iterating leads to unpredictable behaviour — some entries might be skipped, others visited twice, and the loop may even panic. If you need to modify a map based on the entries you visit, collect the keys you want to change in a slice first, then modify the map after the loop.

Iteration order is never guaranteed:

Relying on a specific order leads to bugs that show up only under load or on different machines. If you need sorted output, extract the keys into a slice, sort the slice, and then read the map in that order.

Here’s a complete example that prints a map’s contents in alphabetical key order:

import "sort"
func printSorted(m map[string]int) {
    keys := make([]string, 0, len(m))
    for k := range m {
        keys = append(keys, k)
    }
    sort.Strings(keys)
    for _, k := range keys {
        fmt.Println(k, "=>", m[k])
    }
}

Map iteration is one of the few places where Go’s simplicity creates a small extra step. Sorting the keys yourself takes a few lines, but it makes the intent explicit and forces you to decide whether order matters at all — which often it does not.


Summary

Reading, writing, checking existence, deleting, and iterating are the five verbs of everyday map use. The two points that most often trip people up are the zero‑value default when reading a missing key and the nil‑map write panic. The comma‑ok idiom is the tool that tells you the difference between “absent” and “zero”. The delete function safely removes entries, and for range gives you full traversal, albeit with no ordering promise.