9.1 Creating Maps: map[K]V Literals and make()
Right, let’s talk about maps. You’ve been using arrays and slices, which are great when you want to order things sequentially, like a to-do list. But what about when you want to look things up by a specific key? You don’t want to loop through every item to find the user with ID 42; you want to go directly to the user at users[42]. That’s what a map is for: a lookup table. It’s your Go-to (see what I did there?) data structure for associating one value, the key, with another value. We declare a map type as map[K]V, where K is the type for your keys and V is the type for your values.
The map literal: your quick-start kit
The most straightforward way to create a map is with a literal. It looks a lot like a struct literal but with the keys thrown into the mix. You use this when you know the initial key-value pairs you want to populate your map with at compile time.
// A map of employee IDs (int) to their names (string)
employees := map[int]string{
101: "Alice Anderson",
202: "Bob Barker",
303: "Charlie Chu",
// Note the trailing comma is required. The compiler will throw a fit without it.
}
fmt.Println("Employee 202 is:", employees[202])
// Output: Employee 202 is: Bob Barker
You can make an empty map literal too, which is incredibly useful. This creates a map that’s ready to use, not nil.
// An empty but ready-to-use map
tasks := map[string]bool{}
tasks["Write chapter on maps"] = true
tasks["Achieve world peace"] = false
The make() function: for when you have a hunch
What if you don’t know the initial data, but you have a rough idea of how many items you’re about to stuff into this map? Creating a map with make() allows you to specify an initial capacity hint. This is a performance optimization, not a hard limit.
Think of it like reserving seats at a theater. If you tell the box office you’ve got a party of 100, they’ll rope off a section of roughly that size so you can all sit together. They don’t stop you from bringing 101 friends, but it makes the initial seating more efficient. Similarly, providing a capacity hint to make() tells the runtime to allocate enough buckets for n elements upfront, reducing the number of times it needs to rehash and resize as you add more elements.
// I'm about to load roughly 1000 user records, so I'll give make() a heads-up.
userCache := make(map[int]User, 1000)
// Now I can start populating it without causing too many re-allocations
for i := 0; i < 1000; i++ {
userCache[i] = fetchUserFromDB(i)
}
The key thing to remember: if you have even a vague idea of the final size, use make(map[K]V, n). It’s a cheap win for performance. If you don’t, just use an empty literal or make() without a size. The map will grow automatically and without complaint.
The dreaded nil map: a runtime panic in a trench coat
This is the big gotcha, the one that’ll bite you when you’re least expecting it. A nil map is a map that has been declared but not initialized with a literal or make(). It’s perfectly valid Go code to have one. The problem is, a nil map behaves like an empty map for reads—you can ask it for a key and it will happily return the zero value—but it will absolutely panic if you try to write to it.
var dangerousMap map[string]int // This is a nil map right now
fmt.Println("This will work and print 0:", dangerousMap["nothing"]) // Read: okay
dangerousMap["everything"] = 1 // Write: PANIC!
// panic: assignment to entry in nil map
The error message is actually pretty clear, but you’ll only see it at runtime. The compiler won’t save you here. The best practice is to never use var m map[K]V on its own. If you’re declaring a map variable without populating it immediately, use make():
// The safe way: initialize it, even if it's empty
safeMap := make(map[string]int)
// or
var safeMap = map[string]int{}
This way, you’re guaranteed to have a usable map from the get-go. Consider the var m map[K]V pattern to be functionally useless; it exists mainly for cases where a struct might need a map field that you’ll make() later in a constructor function.
Under the hood: why capacity matters
I told you to use a capacity hint with make(), let’s briefly talk about why it helps. A Go map is implemented as a hash table. When you add a key-value pair, the key is hashed to point to a “bucket.” The map starts with a few buckets. As you add more and more elements, the map becomes crowded, and its efficiency drops. So, the runtime creates a new, larger set of buckets and rehashes all the existing keys into them—a relatively expensive operation.
By providing a capacity, you’re saying, “Hey, I plan to put at least n elements in here.” The runtime can then allocate a number of buckets appropriate for n elements from the start, potentially avoiding several rounds of resizing and rehashing as you populate the map. It doesn’t lock the map to that size, but it makes the initial population much smoother. It’s one of those easy, low-effort optimizations that shows you know what you’re doing.