1. Why do we need map if we already have slices
When you’ve just learned slices, you might get the feeling: “That’s it, I have a list—so I have data.” And that’s true... until the task “find a value by key” shows up. For example: “by the name alice return the phone number”, or “by the product code return the price”, or “by the login return the status”. With a slice, this turns into walking through the entire list with a flashlight: go element by element, compare, and hope you don’t miss it.
map is a data structure for the “key → value” scenario. That is, you say: “the key is the name, the value is the phone number”, and then you can access the phone number by name without manually iterating over the whole list. If you want a down-to-earth analogy: a slice is a box full of notes, while a map is a card catalog where each card sits in a slot by last name. You can find it in the box too, but you’ll grumble.
What the map[K]V type looks like: key and value
Before writing code, it’s useful to get comfortable with the type notation. In Go, the map type looks like this: map[K]V, where K is the key type and V is the value type. If you write map[string]int, it reads as “a table where the key is a string and the value is a number”. If it’s map[int]string, then the opposite: the key is a number and the value is a string.
There is an important restriction here: keys must be comparable, meaning they can be compared using the == and != operators. In plain language, this means: the language must be able to say “are these two keys the same or not”—otherwise the map won’t be able to figure out which “slot” to store the data in. In the official Go materials this concept is described as comparable types.
Let’s lock this in with a simple table (no fanaticism and no deep dive into hash table internals).
| Key type | Can it be a map key? | Why |
|---|---|---|
, , |
yes | comparable ( / ) |
(if all fields are comparable) |
yes | comparability is “composed” from the fields |
(slice) |
no | slices are not comparable via |
(another map) |
no | maps are not comparable |
2. Creating a map: make(...) and the {...} literal
Now let’s move to the most practical part: how to get a map that you can put things into. There are two main ways. The first is to create an empty map with make. The second is to create a map immediately with initial data using a literal.
It’s important to agree with your brain in advance on a simple thing: make(map[K]V) creates a “live” map, ready for writes. But var m map[K]V creates a map variable, and its default value is nil. It’s like “I have a variable for a box, but the box itself isn’t there yet”. You can read from a nil-map, but you cannot write to it (it will end in a runtime panic).
make(map[K]V) — the most common starting point
package main
import "fmt"
func main() {
phones := make(map[string]string)
phones["alice"] = "+1-202-555-0101"
fmt.Println(phones["alice"]) // +1-202-555-0101
}
Here we made a “phone book”: the key is the name, the value is the phone number. Writing is done with a normal assignment by key: phones["alice"] = ....
map[K]V{...} literal — when you have initial data
package main
import "fmt"
func main() {
phones := map[string]string{
"alice": "+1-202-555-0101",
"bob": "+1-202-555-0110",
}
fmt.Println(phones["bob"]) // +1-202-555-0110
}
The literal is handy when you want to quickly hardcode test data or a starting configuration.
4. Reading and writing: m[k] and m[k] = v
It’s easy to start “talking” to a map if you remember two expressions: reading m[k] and writing m[k] = v. Writing is intuitive: put the value into the slot for that key. Reading has a nuance: if the key doesn’t exist, Go will still return a value of type V, but it will be the zero value.
So for map[string]int, when the key is missing you will get 0, for map[string]string — the empty string "", for map[string]bool — false. This isn’t an “error”, it’s standard behavior, but because of it it can sometimes be hard to tell: was the key missing, or did the key exist but the value was the zero value? We’ll learn how to distinguish these cases carefully a bit later using a special idiom, but today it’s important to simply remember: reading from a map does not crash.
Writing a new key and updating an existing one
If the key didn’t exist, it will appear. If the key existed, the value will be overwritten. No separate “add” or “update” operations are needed: one operation solves both scenarios.
package main
import "fmt"
func main() {
phones := make(map[string]string)
phones["alice"] = "111"
phones["alice"] = "222" // overwritten
fmt.Println(phones["alice"]) // 222
}
A small nuance about passing to a function
At this stage, it’s useful to know a practical thing: if you pass a map into a function and modify the map inside the function, the changes will be visible outside. This is very convenient: you can write small helper functions that “extend” a shared map.
package main
import "fmt"
func setPhone(m map[string]string, name string, phone string) {
m[name] = phone
}
func main() {
phones := make(map[string]string)
setPhone(phones, "bob", "333")
fmt.Println(phones["bob"]) // 333
}
We’re not going into deep theory about “what’s inside a map” yet; we’ll just use this as a fact that makes code simpler.
5. len(m) and delete(m, k): how many elements and how to delete
When you store data in a map, sooner or later you’ll want two things: understand “how many entries are there right now” and “delete an entry”. For the count there is the built-in function len. For deletion there is the built-in function delete.
With len everything is simple: it’s the number of “key → value” pairs that currently live in the map. With delete there’s a nice property: it’s safe to delete even if the key doesn’t exist. That is, delete doesn’t complain, doesn’t return an error, and doesn’t create drama like “why are you deleting something that isn’t there”. It just does nothing—and moves on.
Counting elements with len
package main
import "fmt"
func main() {
phones := map[string]string{"alice": "111", "bob": "222"}
fmt.Println(len(phones)) // 2
phones["carol"] = "333"
fmt.Println(len(phones)) // 3
}
Deleting with delete
package main
import "fmt"
func main() {
phones := map[string]string{"alice": "111", "bob": "222"}
delete(phones, "bob")
delete(phones, "bob") // deleting again is fine
fmt.Println(len(phones)) // 1
fmt.Println(phones["bob"]) // "" (key is missing, returned zero value)
}
6. nil-map: what you can do and what you can’t
The topic of nil often causes philosophical sadness for beginners: “how can it be—there is a variable, but there is no value?”. In fact, nil is a normal zero value for some types in Go. And map has it too: a nil-map.
The practical rule sounds like this: you can read from a nil-map, but you cannot write. Reading will produce the zero value. len(nilMap) will return 0. A range over a nil-map will simply perform 0 iterations (i.e., it will be “silent”). But m[k] = v on a nil-map will lead to a runtime panic.
Demonstrating a nil-map
package main
import "fmt"
func main() {
var phones map[string]string // nil-map
fmt.Println(phones == nil) // true
fmt.Println(len(phones)) // 0
fmt.Println(phones["x"]) // "" (zero value)
// phones["x"] = "123" // panic: assignment to entry in nil map
}
Yes, this is the case where the program “explodes” not at compile time but at runtime. That’s why it’s important to recognize a nil-map and initialize it in time using make.
Protective initialization before writing
Sometimes a map can come to you as a function argument, and you’re not sure whether it has been initialized or not. Then you can use a simple defensive technique: if m == nil, create it.
package main
import "fmt"
func safeSet(m map[string]string, k, v string) map[string]string {
if m == nil {
m = make(map[string]string)
}
m[k] = v
return m
}
func main() {
var phones map[string]string
phones = safeSet(phones, "alice", "111")
fmt.Println(phones["alice"]) // 111
}
Notice the trick: the function returns the map back. This is convenient when you need to “recover” from nil and keep going.
7. Example: an in-memory “phone book”
Let’s assemble a small piece of a future application—an in-memory phone book. We’re not building a complex interface or a menu yet (it’s easy to turn that into a separate epic), but we’ll already learn how to store, update, and delete contacts.
The idea is: we have a map[string]string where the key is the contact name and the value is the phone number. We read a command (add or del) and a name, and sometimes also a phone number. At this point Go already looks like a normal working language, not like a “guide to curly braces”.
Adding or updating a contact
package main
import (
"fmt"
)
func main() {
phones := make(map[string]string)
var name, phone string
fmt.Scan(&name, &phone) // input: alice 111
phones[name] = phone
fmt.Println(phones[name]) // 111
}
It’s important to notice the calm mechanics here: we don’t check “did the key exist?”. We just assign. If the contact already existed, it will be updated. If not, it will be created.
Deleting a contact by name
package main
import "fmt"
func main() {
phones := map[string]string{"alice": "111", "bob": "222"}
var name string
fmt.Scan(&name) // input: bob
delete(phones, name)
fmt.Println(len(phones)) // 1
}
This is very Go-like: delete doesn’t require “if it exists, then delete it”. You don’t need to add extra checks or complicate the code.
A small diagram of the map lifecycle
When you’re first getting used to map, it helps to keep a simple “flowchart” of usage in mind. It’s not about internal implementation, but about how you typically write code.
flowchart TD
A[Declared a map variable] --> B{Is it nil?}
B -- yes --> C["Called make(map[K]V)"]
B -- no --> D[You can read and write]
C --> D
D --> E["Write: m[k] = v"]
D --> F["Read: v := m[k]"]
D --> G["Delete: delete(m, k)"]
D --> H["Size: len(m)"]
If you follow this logic, map will become a very predictable thing, not a “magic dictionary that sometimes crashes”.
8. Common mistakes when working with map
Mistake #1: writing to a nil-map.
This is the most common “production” problem, because the compiler doesn’t catch it: the program builds, starts, and then crashes with a panic on the first write. The fix is simple: before writing, the map must be created with make(...) or a literal. If the map comes in as an argument and can be nil, do a defensive initialization: if m == nil { m = make(...) }.
Mistake #2: expecting that reading a missing key will produce an error.
Beginners often expect m["unknown"] to somehow “report” that the key is missing. But that’s not how map works: reading returns the zero value. This is convenient for some patterns, but sometimes it creates ambiguity. It’s important to at least remember this fact, and you’ll learn a separate technique a bit later for cleanly distinguishing “no such key” from “zero value”.
Mistake #3: trying to use a non-comparable type as a key.
Sometimes you want to use a slice or another map as a key: “but that’s data too”. But the key must be comparable (==/!=), otherwise Go cannot match keys correctly. This restriction is fundamental: the language directly defines the notion of type comparability, and map relies on it.
Mistake #4: confusing len(map) with the “last index”, like in an array/slice.
A map is not an array or a slice. It has no indices, no “zero element”, no “last element”. len(m) is simply the number of pairs. Any logic like “I’ll iterate from 0 to len(m)” doesn’t apply to a map.
Mistake #5: extra checks before delete.
It’s very tempting to write: “if the key exists, then delete it”. But delete is safe for a missing key, and the additional check often only bloats the code and increases the chance of mistakes. If your goal is simply “remove the entry if it exists”, call delete directly.
GO TO FULL VERSION