Chapter 5: Keys, Values, and the Art of Looking Things Up
Friday morning arrived with clear skies. Ethan descended to the archive carrying the usual coffee tray and a small paper bag.
Eleanor looked up from her desk. "What did you bring today?"
"Black and white cookies. I figured we've covered enough territory this week to deserve them."
She smiled and took one. "The optimist's cookie—never commit to just one flavor. Appropriate for today's topic."
"Which is?"
"Maps. Or as other languages call them: dictionaries, hash tables, associative arrays. The data structure for when you need to look things up."
Eleanor opened her laptop and typed:
package main
import "fmt"
func main() {
var ages map[string]int
fmt.Println(ages)
}
She ran it:
map[]
"This declares a map. map[string]int means: a map where the keys are strings and the values are integers. Think of it like a real dictionary—you look up a word (the key) and get a definition (the value)."
Ethan studied the declaration. "So I could store people's ages?"
"Exactly. But watch what happens if we try to use this map:"
package main
import "fmt"
func main() {
var ages map[string]int
ages["Alice"] = 30 // This will panic!
fmt.Println(ages)
}
"If you run this, Go panics: 'assignment to entry in nil map.' The map is declared but not initialized. It's nil—you can read from a nil map, and you'll get zero values back, but you cannot write to it."
"How do we fix it?"
Eleanor typed:
package main
import "fmt"
func main() {
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 25
ages["Charlie"] = 35
fmt.Println(ages)
fmt.Println("Alice's age:", ages["Alice"])
}
She ran it:
map[Alice:30 Bob:25 Charlie:35]
Alice's age: 30
"Now it works. make(map[string]int) creates an initialized, empty map ready to use. We add entries with ages["Alice"] = 30 and retrieve them with ages["Alice"]."
"What if I ask for someone who's not in the map?"
"Great question." Eleanor modified the code:
package main
import "fmt"
func main() {
ages := make(map[string]int)
ages["Alice"] = 30
fmt.Println("Alice's age:", ages["Alice"])
fmt.Println("Bob's age:", ages["Bob"])
}
Output:
Alice's age: 30
Bob's age: 0
"It returns zero. Not an error, not a panic—just the zero value for integers. Go's philosophy: maps return the zero value of the value type for missing keys—0 for numbers, false for booleans, empty string for strings."
Ethan frowned. "But then how do I know if someone's actually zero versus not in the map?"
Eleanor's eyes gleamed. "This is where Go gets elegant. Watch:"
package main
import "fmt"
func main() {
ages := make(map[string]int)
ages["Alice"] = 30
ages["Bob"] = 0 // Bob is actually zero
// The comma-ok idiom
age, ok := ages["Alice"]
fmt.Println("Alice:", age, "found:", ok)
age, ok = ages["Bob"]
fmt.Println("Bob:", age, "found:", ok)
age, ok = ages["Charlie"]
fmt.Println("Charlie:", age, "found:", ok)
}
She ran it:
Alice: 30 found: true
Bob: 0 found: true
Charlie: 0 found: false
"When you access a map with two variables—age, ok := ages["Alice"]—you get both the value AND a boolean indicating whether the key exists. This is called the 'comma-ok idiom.'"
"So ok is true if the key exists, even if the value is zero?"
"Exactly. Bob is in the map with value 0, so ok is true. Charlie isn't in the map, so ok is false."
Eleanor drew in her notebook:
Map Access Patterns:
1. Simple access (returns zero value if missing):
value := myMap[key]
2. Comma-ok idiom (check existence):
value, ok := myMap[key]
if ok {
// key exists, use value
} else {
// key doesn't exist
}
"Let's look at common map operations:"
package main
import "fmt"
func main() {
// Creating and initializing a map
scores := map[string]int{
"Alice": 95,
"Bob": 87,
"Carol": 92,
}
// Adding or updating
scores["David"] = 88
scores["Alice"] = 97 // Update Alice's score
// Checking if key exists
score, exists := scores["Eve"]
if exists {
fmt.Println("Eve's score:", score)
} else {
fmt.Println("Eve not found")
}
// Deleting a key
delete(scores, "Bob")
// Iterating over a map
fmt.Println("\nAll scores:")
for name, score := range scores {
fmt.Println(name, "scored", score)
}
// Length of map
fmt.Println("\nTotal students:", len(scores))
}
Eleanor ran the code:
Eve not found
All scores:
Alice scored 97
Carol scored 92
David scored 88
Total students: 3
"Notice a few things. First, the map literal—map[string]int{...}—automatically initializes the map, just like make does. It's ready to use immediately. Second, delete(scores, "Bob") removes an entry. Notice it's a built-in function, not a method—you call delete(map, key), not map.delete(key) like in other languages. Calling delete on a key that doesn't exist is safe—it does nothing, doesn't error."
"And the order when we iterate?"
"Random. Maps in Go are unordered. Every time you iterate with range, you might get a different order. It's intentional—Go randomizes it to prevent you from depending on order."
Ethan thought about this. "Why would I depend on order?"
"Because humans like patterns. If iteration happened to be alphabetical during testing, you might write code that assumes alphabetical order. Then in production, something breaks. Go says: 'Don't assume. I'll randomize it so you can't.'"
Eleanor opened a new file. "Maps are reference types, like slices. When you pass a map to a function, you're passing a reference. Changes inside the function affect the original:"
package main
import "fmt"
func addStudent(grades map[string]int, name string, grade int) {
grades[name] = grade
}
func main() {
grades := make(map[string]int)
grades["Alice"] = 90
fmt.Println("Before:", grades)
addStudent(grades, "Bob", 85)
fmt.Println("After:", grades)
}
Output:
Before: map[Alice:90]
After: map[Alice:90 Bob:85]
"The function modified the original map. No pointers needed—maps are already references."
"Like slices?"
"Similar, but maps have an important difference. With slices, you need to return the result of append because the backing array might change. With maps, you never worry about that. Maps handle their own growth automatically, and the reference stays valid."
Eleanor pulled out her checking paper. "Let's trace through a more complex example—counting word occurrences:"
package main
import (
"fmt"
"strings"
)
func main() {
text := "the quick brown fox jumps over the lazy dog the fox"
words := strings.Fields(text)
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
fmt.Println("Word counts:")
for word, count := range counts {
fmt.Printf("%s: %d\n", word, count)
}
}
"This is a classic map pattern. We split text into words, then for each word, we increment its count. The beautiful part? counts[word]++ works even if word isn't in the map yet."
She ran it:
Word counts:
the: 3
fox: 2
quick: 1
brown: 1
jumps: 1
over: 1
lazy: 1
dog: 1
"When we do counts[word]++, if word doesn't exist, Go returns 0 (the zero value for int), then increments it to 1. It's elegant—no special 'check if exists first' logic needed."
Let's visualize how this works:
Initial state:
counts = map[] (empty)
Processing "the" (first occurrence):
counts["the"] doesn't exist
→ returns 0
→ increment to 1
→ counts = map[the:1]
Processing "quick":
counts["quick"] doesn't exist
→ returns 0
→ increment to 1
→ counts = map[the:1 quick:1]
Processing "the" (second occurrence):
counts["the"] exists with value 1
→ returns 1
→ increment to 2
→ counts = map[the:2 quick:1]
And so on...
Ethan typed along, watching the word counts appear. "This would be painful without maps."
"In languages without maps, you'd use arrays and search through them. Slow. With maps, lookup is fast—usually constant time, regardless of how many entries you have."
Eleanor closed her laptop. "That's the essence of maps. Keys map to values. Missing keys return zero values. The comma-ok idiom lets you distinguish missing from zero. Maps are unordered. Maps are references. And one more thing—map keys must be comparable types. You can use strings, integers, bools, but not slices or other maps as keys."
She finished her cookie. "Next week: structs. How to create your own types and group related data together."
Ethan gathered the cups. "Eleanor?"
"Yes?"
"The comma-ok pattern—value, ok := map[key]—we saw something similar with type assertions, right?"
Eleanor raised an eyebrow. "We haven't covered type assertions yet. But yes, Go uses the comma-ok idiom in several places. It's a pattern: when an operation might fail or might not have a value, Go returns both the result and a boolean. Explicit over implicit."
"Everything in Go seems to follow patterns."
"That's by design. Go has a small number of concepts, combined in consistent ways. Learn the patterns, and the language feels predictable." She paused. "Some languages have hundreds of features. Go has dozens. But those dozens are composed carefully, so they work together."
Ethan climbed the stairs, thinking about keys and values and the way Go made looking things up feel natural. In Python, dictionaries threw exceptions for missing keys unless you used .get(). In Go, missing keys just returned zero, and if you needed to know, you asked with comma-ok.
Maybe that was the pattern: Go trusted you to handle the normal case simply, and gave you tools to handle the special cases explicitly. Zero values for missing keys worked most of the time. When it didn't, comma-ok was right there.
Key Concepts from Chapter 5
Maps: Collection of key-value pairs. Declared as map[KeyType]ValueType.
Map initialization: Use make(map[KeyType]ValueType) to create a usable map. Declared but uninitialized maps are nil and will panic on assignment.
Nil maps: You can read from a nil map (returns zero values), but you cannot write to it.
Map literals: Initialize with data using map[KeyType]ValueType{key1: value1, key2: value2}. Like make, map literals automatically initialize the map.
Accessing values: value := myMap[key] returns the value, or the zero value if key doesn't exist.
Comma-ok idiom: value, ok := myMap[key] returns both the value and a boolean indicating if the key exists.
Adding/updating: myMap[key] = value adds a new entry or updates an existing one.
Deleting: delete(myMap, key) removes a key-value pair from the map. Safe to call on non-existent keys.
Iteration: Use for key, value := range myMap to iterate. Order is randomized and unpredictable.
Length: len(myMap) returns the number of key-value pairs.
Reference type: Maps are references. Passing a map to a function allows the function to modify the original map.
Zero values: Missing keys return the zero value of the value type (0 for int, "" for string, false for bool, etc.).
Automatic growth: Maps grow automatically as needed. No capacity management required like with slices.
Key type restrictions: Map keys must be comparable types—strings, integers, bools are valid; slices and maps cannot be keys.
Next chapter: Structs—where Ethan learns to create custom types, and Eleanor explains why Go's approach to data modeling is so straightforward.
Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.
Top comments (0)