Start learning free

LESSON 100 OF 338 · ARRAYS I

Maps and the ok Idiom in Go: Telling Zero from Missing

Lesson Read the full explanation

A map is Go's built-in dictionary: it stores key: value pairs and finds a value by its key instantly. The type map[string]int reads left to right as "keys are strings, values are ints". The easiest way to create one is a map literal, which creates and fills the map in a single line:

scores := map[string]int{"Alice": 92, "Bob": 77}
fmt.Println(scores["Alice"])   // prints 92
scores["Bob"] = 80             // update a value

One warning before we go on: a map that is only declared (var scores map[string]int) is nil — reading from it is fine, but writing into it stops your program with this exact panic:

panic: assignment to entry in nil map

goroutine 1 [running]:
main.main()
	/tmp/sandbox/prog.go:5 +0x28

Always create maps with a literal {...} or with make(map[string]int).


The trap:

In many languages, asking a dictionary for a key it doesn't have crashes (Python raises KeyError) or returns a special "nothing" value. Go does neither. Look up a missing key and you silently get the value type's zero value — 0 for numbers, "" for strings, false for bools:

fmt.Println(scores["Carol"])   // Carol isn't in the map...
// output: 0                   // ...no crash, no error — just 0

That 0 is a trap: did Carol score zero points, or is Carol simply not in the map? The lookup alone cannot tell you, and this silent ambiguity causes real bugs.

The fix — the "comma ok" idiom: a map read can return a second value, a bool that is true only if the key was actually present:

score, ok := scores["Carol"]
if !ok {
    fmt.Println("Carol is not in the map")
}

By convention that variable is named ok — you will see v, ok := m[key] everywhere in real Go code.


Assignment
Follow the Coding Tutorial to look up one present key and one missing key, printing the value and the ok flag both times:

  • Create scores := map[string]int{"Alice": 92, "Bob": 77}.
  • Look up "Alice" with aliceScore, ok := scores["Alice"] and print both values.
  • Look up "Carol" (who is not in the map) with carolScore, ok := scores["Carol"] and print both values — note the zero value and ok = false.

The exact expected output is:

aliceScore = 92 ok = true
carolScore = 0 ok = false

Hint
Print with fmt.Println("aliceScore =", aliceScore, "ok =", ok) — Println inserts a space between arguments. The second lookup can reuse ok with := because carolScore is a new variable.


STEP 1 OF 5
    Ctrl + Enter to check

    GO · ARRAYS I

    Maps And The Ok Idiom in Go, explained

    What a map is

    A map is Go's built-in dictionary. It stores key: value pairs and finds a value by its key without scanning. The type map[string]int reads left to right as "keys are strings, values are ints". The simplest way to create one is a map literal, which creates and fills the map in one statement:

    scores := map[string]int{"Alice": 92, "Bob": 77}
    fmt.Println(scores["Alice"]) // 92
    scores["Bob"] = 80           // update a value
    

    Map keys must be comparable with ==, so strings, numbers and booleans work, while slices don't.

    Nil maps: reading is fine, writing panics

    A map that is only declared is nil:

    var m map[string]int
    fmt.Println(m["x"], len(m), m == nil) // 0 0 true
    m["x"] = 1                            // panic: assignment to entry in nil map
    

    Reading from a nil map works and behaves like reading from an empty one. Writing stops the program with panic: assignment to entry in nil map. Create maps with a literal or with make(map[string]int), and the problem never appears.

    The trap: a missing key gives the zero value

    In many languages, asking a dictionary for a key it doesn't have raises an error or returns a special "nothing" value. Go does neither. A lookup of a missing key returns the zero value of the value type: 0 for numbers, "" for strings, false for bools.

    fmt.Println(scores["Carol"]) // 0
    

    That 0 is ambiguous. Either Carol is not in the map, or she is there with a score of zero. The lookup alone can't tell you which.

    The comma-ok idiom

    A map read can return a second value, a bool that is true only when the key was present:

    score, ok := scores["Carol"]
    if !ok {
        fmt.Println("Carol is not in the map")
    }
    

    The second variable is called ok by convention, and you will see v, ok := m[key] throughout real Go code. When you only need the check inside an if, declare the variables in its header so they stay scoped to the statement:

    if v, found := scores["Bob"]; found {
        fmt.Println("Bob has", v)
    }
    

    A key stored with the zero value still reports ok as true. After scores["Dan"] = 0, the lookup d, ok := scores["Dan"] gives 0 true, while Carol gives 0 false. That is exactly the difference the idiom exists to reveal.

    The exercise, step by step

    1. Create the map with a literal holding Alice and Bob. The literal makes sure it is never nil.
    2. Look up Alice with two results: aliceScore, ok := scores["Alice"].
    3. Print the label, the score, the label and ok as four arguments to fmt.Println. It puts a space between arguments, so the labels need no trailing space.
    4. Look up Carol the same way, into carolScore and ok. Using := with ok again is legal because carolScore on the left is new. Go reports an error only when a := declares no new variable at all, and here ok is simply assigned.
    5. Print both values again. Carol gives the zero value and false.

    The solution in Go

    scores := map[string]int{"Alice": 92, "Bob": 77}
    aliceScore, ok := scores["Alice"]
    fmt.Println("aliceScore =", aliceScore, "ok =", ok)
    carolScore, ok := scores["Carol"]
    fmt.Println("carolScore =", carolScore, "ok =", ok)
    

    The output is:

    aliceScore = 92 ok = true
    carolScore = 0 ok = false
    

    Why the idiom is more than a style rule

    The zero-value behaviour is convenient as well as risky. A counter map needs no setup, because a missing key reads as 0 and the increment writes back a real entry:

    counts := map[string]int{}
    counts["a"]++
    counts["a"]++
    fmt.Println(counts) // map[a:2]
    

    Here the zero value is exactly what you want, so no check is needed. The comma-ok form is for the cases where "absent" and "zero" mean different things: a score of zero against a student who never took the test, or a setting that is switched off against one that was never configured. When both cases would be handled the same way, the plain lookup is enough.

    Deleting also follows the "no surprises" style. delete(scores, "Bob") removes the entry, and deleting a key that isn't there does nothing and doesn't panic. Afterwards the comma-ok lookup for that key returns 0 false.

    Common mistakes

    • Treating 0 as "not found". This is wrong whenever zero is a legal value. Check ok instead.
    • Unused variables. Go refuses to compile a variable that is declared and never used. If you only care about presence, write _, ok := scores["Carol"].
    • Writing to a nil map. var m map[string]int followed by an assignment panics at run time.
    • Relying on iteration order. Looping over a map with range visits the entries in an unspecified order that can differ between runs.

    Key takeaways

    • A missing key returns the zero value, never an error.
    • v, ok := m[key] tells a missing key apart from a stored zero.
    • Create maps with a literal or make. A nil map can be read but not written.
    Keep going with GoAbout 340 step-by-step lessons, from your first line of code to coding interviews. Free to start.
    Start learning free