Golang - Data Structures - Maps In-Depth
Introduction
A map
in Go is a powerful, built-in data structure that associates keys of one type with values of another. It is an unordered collection, and it provides fast lookups, additions, and deletions. Maps are one of the most commonly used data structures in Go.
Analogy: Think of a map
as a dictionary or a phone book. You look up a word (the key) to find its definition (the value). The keys must be unique.
Declaring and Initializing Maps
There are several ways to create a map.
package main
import "fmt"
func main() {
// 1. Using the `make` function (most common)
// Creates a map with string keys and int values
ags := make(map[string]int)
ags["Alice"] = 30
ags["Bob"] = 25
// 2. Using a map literal
// Creates and initializes a map in one step
scores := map[string]int{
"Alice": 95,
"Bob": 80,
}
// 3. Declaring a nil map
var nilMap map[string]int
fmt.Println("nilMap is nil?", nilMap == nil) // true
// NOTE: You cannot add keys to a nil map! This will cause a runtime panic.
// nilMap["Charlie"] = 50 // PANIC!
fmt.Println(ages)
fmt.Println(scores)
}
Key Takeaway: You must initialize a map before you can add elements to it. The idiomatic way is with make
or a map literal.
Core Map Operations
1. Adding or Updating Elements
The syntax for adding and updating is the same.
m := make(map[string]int)
m["key1"] = 10 // Add
m["key1"] = 20 // Update
0221 - Data Structures - Map Internals and Performance
2. Retrieving Elements
You retrieve a value using its key.
value := m["key1"] // value will be 20
3. The “Comma Ok” Idiom
What if a key doesn’t exist? Accessing it will return the zero value for the value type (e.g., 0
for int
, ""
for string
, false
for bool
). This can be ambiguous. Did you store a zero, or is the key missing?
To solve this, Go provides the “comma ok” idiom, which returns a second, boolean value indicating if the key was found.
package main
import "fmt"
func main() {
scores := map[string]int{
"Alice": 95,
"Bob": 0, // Bob's score is explicitly zero
}
// Check for a key that exists
score, ok := scores["Alice"]
fmt.Printf("Alice's score: %d, Found: %t\n", score, ok) // 95, true
// Check for a key that doesn't exist
score, ok = scores["Charlie"]
fmt.Printf("Charlie's score: %d, Found: %t\n", score, ok) // 0, false
// Check for a key whose value is the zero value
score, ok = scores["Bob"]
fmt.Printf("Bob's score: %d, Found: %t\n", score, ok) // 0, true
}
Rule of Thumb: Always use the “comma ok” idiom when you need to distinguish between a missing key and a key that has a zero value.
4. Deleting Elements
Use the built-in delete
function.
delete(scores, "Alice") // Removes the key "Alice"
Deleting a non-existent key does nothing and does not cause a panic.
5. Iterating Over a Map
Use a for...range
loop.
for key, value := range scores {
fmt.Printf("Key: %s, Value: %d\n", key, value)
}
CRITICAL: The iteration order of a Go map is not guaranteed. The order can be different every time you run the program. This is an intentional design choice to prevent developers from relying on a specific iteration order. If you need a stable order, you must extract the keys, sort them, and then iterate.
import "sort"
var keys []string
for k := range scores {
keys = append(keys, k)
}
sort.Strings(keys) // Sort the keys
for _, k := range keys {
fmt.Printf("Key: %s, Value: %d\n", k, scores[k]) // Iterate in sorted order
}
Map Keys
- A map key can be of any type that is comparable (i.e., you can use
==
and!=
on it). This is a fundamental requirement because Go’s map is a hash table. To find a value, Go must be able to:- Compute a hash of the key to quickly locate the right “bucket.”
- Compare the given key with the keys in that bucket using
==
to find an exact match.
- This includes most built-in types:
string
,int
,float
,bool
, pointers, channels, and interface types. - Slices, maps, and functions cannot be map keys because the
==
operator is not defined for them.
Using Structs as Keys
A struct can be a map key, but only if all of its fields are comparable. If a struct contains a slice, map, or function field, it cannot be used as a key.
package main
import "fmt"
// This struct IS comparable because its fields (int, string) are.
type user struct {
id int
name string
}
func main() {
// We can use the `user` struct as a map key
lastLogin := make(map[user]string)
u1 := user{id: 1, name: "Alice"}
u2 := user{id: 2, name: "Bob"}
lastLogin[u1] = "2025-09-07T10:00:00Z"
lastLogin[u2] = "2025-09-07T10:05:00Z"
fmt.Println("Alice's last login:", lastLogin[u1])
// A struct with a slice field is NOT comparable.
// type userWithPerms struct {
// id int
// perms []string // This field makes the struct non-comparable
// }
// m := make(map[userWithPerms]bool) // This would cause a compile-time error
}
Arrays vs. Slices as Keys
This is a critical distinction that often comes up in interviews:
- Arrays CAN be keys (as long as their element type is also comparable). An array has a fixed size and value semantics, so two arrays are equal if they have the same length and their corresponding elements are equal.
- As noted, slices CANNOT be keys.
// An array can be a key
coordinates := make(map[[2]int]string)
coordinates[[2]int{10, 20}] = "Treasure"
fmt.Println(coordinates[[2]int{10, 20}]) // Output: Treasure
Pointers as Keys (A Common Gotcha)
While you can use pointers as map keys, it’s important to understand that the key is the pointer’s memory address, not the value it points to. Two different pointers that point to identical values are treated as two distinct keys.
package main
import "fmt"
func main() {
a := 5
b := 5
// p1 and p2 point to different memory addresses, even though the values (5) are the same.
p1 := &a
p2 := &b
m := make(map[*int]string)
m[p1] = "Value for p1"
fmt.Println("Using p1:", m[p1]) // "Value for p1"
fmt.Println("Using p2:", m[p2]) // "" (empty string), because p2 is a different key
}
Key Takeaway: Use pointer keys only when you need to associate data with a specific instance of an object in memory. In most cases, using a value type (like a struct or an ID) as a key is safer and more predictable.