When I started learning Go, I learned about arrays and slices, which are useful for storing sequences of values.
But what if I want to store information in a different way?
Imagine I want to count how many times each word appears in a file. I need a way to associate every word with a number.
For example:
"Go" → 5
"program" → 3
"slice" → 7
This is exactly the kind of problem Go’s built-in map type is designed to solve.

In this post, I’ll explore how maps work, how to create and modify them, why nil maps behave differently from empty maps, and how to use maps to build a simple word-counting program.
1. What Is a Map in Go?
A map is a collection of key-value pairs.
Instead of accessing elements using numerical indexes like we do with arrays and slices, we access values using keys.
For example, a map can associate a person’s name with their age:
ages := map[string]int{
"Alice": 25,
"Bob": 30,
}
Here:
- "Alice" and "Bob" are keys.
- 25 and 30 are the corresponding values.
- The key type is string.
- The value type is int.
Conceptually, we can think of the map as:
Key Value
----------------
Alice → 25
Bob → 30
This is different from arrays and slices, where elements are stored in a sequence.
Maps are designed for looking up values by key, inserting new key-value pairs, and removing entries.
Internally, maps are commonly implemented using a data structure called a hash table, although Go handles those implementation details for us.
We simply use the map.
2. Declaring a Map
The syntax for declaring a map is:
map[keyType]valueType
Let’s look at some examples.
var a map[string]int
var b map[int]string
The first declaration creates a map type whose keys are strings and whose values are integers.
The second creates a map type whose keys are integers and whose values are strings.
We can also declare a map using the short variable declaration syntax:
m := map[string]int{
"Go": 10,
"Python": 20,
}
Now we have a map that associates programming language names with integer values.
We can access a value using its key:
fmt.Println(m["Go"])
Output:
10
The key is placed inside square brackets, just as an index is used with a slice. The difference is that the key can be a value of the map’s declared key type, rather than necessarily an integer index.
3. Creating Maps with make
There is another way to create a map: using the built-in make function.
m := make(map[string]int)
This creates an initialized, empty map that we can use to store key-value pairs.
Let’s see a complete example.
package main
import "fmt"
func main() {
m := make(map[string]int)
m["Go"] = 10
m["Python"] = 20
fmt.Println(m)
}
The map now contains two entries.
We can also create an empty map using a map literal:
m := map[string]int{}
Both approaches give us an initialized map that can accept new entries.
The important distinction is between an initialized empty map and a nil map.
4. Nil Maps vs. Empty Maps
This is one of the interesting behaviors of maps in Go.
Consider the following declaration:
var m map[string]int
The variable m is a map variable, but its value is nil.
It does not refer to an initialized map data structure.
We can check this:
fmt.Println(m == nil)
Output:
true
Now let’s create another map:
p := make(map[string]int)
fmt.Println(p == nil)
Output:
false
Both maps are empty, but they are not the same.
The first is a nil map. The second is an initialized empty map.
Here’s the important part:
We can read from a nil map, but we cannot write to a nil map.
Let’s understand what that means.
5. Reading from a Map
Suppose we create a map and try to read a key that doesn’t exist.
package main
import "fmt"
func main() {
m := make(map[string]int)
fmt.Println(m["Go"])
}
Output:
0
Why do we get zero?
In Go, when we read a key that does not exist, the map returns the zero value of the map’s value type.
Here, the value type is int, and the zero value of an integer is 0.
Let’s look at a few examples.
m := make(map[string]int)
fmt.Println(m["missing"])
Output:
0
For a map whose value type is string, a missing key returns the empty string:
m := make(map[string]string)
fmt.Println(m["missing"])
Output:
The value is an empty string.
This behavior also applies to nil maps.
var m map[string]int
fmt.Println(m["Go"])
Output:
0
Reading from a nil map is perfectly valid. Go returns the zero value of the map’s value type.
6. Writing to a Nil Map Causes a Panic
Now let’s try to write to a nil map.
package main
func main() {
var m map[string]int
m["Go"] = 10
}
This causes a runtime panic because the map has not been initialized.
To store values, we need to initialize the map first.
For example:
package main
import "fmt"
func main() {
var m map[string]int
m = make(map[string]int)
m["Go"] = 10
fmt.Println(m)
}
Output:
map[Go:10]
The call to make initializes the map so that it can store entries.
This is why you’ll often see maps created with make before they are used for writing.
7. Updating and Deleting Map Entries
Maps are useful because we can insert, update, and remove entries.
To insert a new entry, we use an assignment:
m := make(map[string]int)
m["Go"] = 10
m["Python"] = 20
If we assign a new value to an existing key, the value is updated.
m["Go"] = 15
Now the entry for "Go" has the value 15.
To remove an entry, Go provides the built-in delete function.
delete(m, "Python")
This removes the entry associated with the key "Python".
We can also find the number of entries in a map using len:
fmt.Println(len(m))
The len function returns the number of key-value pairs currently stored in the map.
For example:
package main
import "fmt"
func main() {
m := make(map[string]int)
m["Go"] = 10
m["Python"] = 20
fmt.Println(len(m))
delete(m, "Python")
fmt.Println(len(m))
}
Output:
2
1
8. Map Assignment and Shared Data
Maps behave differently from arrays when we assign them to another variable.
Consider this example:
package main
import "fmt"
func main() {
a := map[string]int{
"Go": 10,
}
b := a
b["Go"] = 100
fmt.Println(a)
fmt.Println(b)
}
Output:
map[Go:100]
map[Go:100]
Both variables refer to the same underlying map data.
When we write:
b := a
we are not creating a completely independent map containing a separate copy of all the entries.
Instead, the map value is copied, and both variables refer to the same map.
That means modifications made through one variable can be observed through the other.
This is important to understand when maps are passed around in a program.
9. Map Keys Must Be Comparable
Not every Go type can be used as a map key.
The key type must support comparison, because Go needs to be able to determine whether two keys are equal.
For example, strings can be used as map keys:
m := map[string]int{
"Go": 10,
}
Strings are comparable, so Go can compare one string key with another.
Integers can also be used as keys:
m := map[int]string{
1: "One",
2: "Two",
}
But slices cannot be used as map keys.
For example, this is invalid:
m := map[[]int]string{}
A slice is not a comparable type, so it cannot be used as a map key.
This is an important connection between slices and maps: although slices are excellent for storing sequences, they cannot serve as map keys.
10. Checking Whether a Key Exists: The Comma-OK Idiom
There is one problem with reading from maps.
Suppose we have this map:
m := map[string]int{
"Go": 0,
}
Now consider these two expressions:
fmt.Println(m["Go"])
fmt.Println(m["Python"])
Output:
0
0
Both expressions return zero.
But there is an important difference:
- "Go" exists in the map and has a value of 0.
- "Python" does not exist in the map.
If we only read the value, we cannot distinguish these two cases.
Go provides a second return value that tells us whether the key exists.
This is commonly called the comma-ok idiom.
value, ok := m["Go"]
fmt.Println(value, ok)
Output:
0 true
The first result is the value associated with the key.
The second result is a boolean indicating whether the key exists.
Let’s try a missing key:
value, ok := m["Python"]
fmt.Println(value, ok)
Output:
0 false
Now we can tell the difference between a key that exists with a zero value and a key that doesn’t exist.
Using comma-ok in an if statement
We can use the second result directly in a conditional.
if value, ok := m["Go"]; ok {
fmt.Println("Found:", value)
} else {
fmt.Println("Key not found")
}
Output:
Found: 0
This is particularly useful when a missing key has a meaningful zero value, such as zero for an integer or an empty string for a string.
11. Building a Word-Counting Program
Now let’s bring these ideas together.
Remember the original problem: counting how many times each word appears in a file or a sequence of text.
A map is a natural solution because we can use each word as a key and its count as the value.
For example:
Go is fun and Go is useful
We want to count the occurrences of each word.
A map might store the result like this:
Go → 2
is → 2
fun → 1
and → 1
useful → 1
Let’s write a small program.
package main
import (
"fmt"
"strings"
)
func main() {
text := "Go is fun and Go is useful"
words := strings.Fields(text)
counts := make(map[string]int)
for _, word := range words {
counts[word]++
}
fmt.Println(counts)
}
Output:
map[Go:2 and:1 fun:1 is:2 useful:1]
Let’s understand the important parts.
Splitting the text into words
words := strings.Fields(text)
This splits the text into fields, giving us a slice of words.
Creating the map
counts := make(map[string]int)
We create a map whose keys are strings and whose values are integers.
The string represents a word, and the integer represents how many times that word appears.
Counting the words
for _, word := range words {
counts[word]++
}
For every word, we increment its count.
If the word is not already in the map, reading its value returns the zero value for int, which is 0.
Then ++ increments that value to 1.
If the word already exists, its count is incremented.
This is a useful example of how Go’s map zero-value behavior makes code simple and convenient.
12. Sorting Map Results
One thing to remember is that maps are not sequences like arrays and slices.
If we want to display the word counts in a particular order, we need to organize the data separately.
One approach is to copy the map entries into a slice and sort that slice.
For example, we can define a small struct:
type Entry struct {
Word string
Count int
}
Then we can use a slice of Entry values to hold the map entries.
var entries []Entry
for word, count := range counts {
entries = append(entries, Entry{
Word: word,
Count: count,
})
}
Now entries is a slice containing the words and their counts.
We can sort this slice using Go’s sorting facilities.
The important idea is that the map is used for efficient key-based counting, while the slice is used when we want to organize the results into a sequence.
This demonstrates how maps and slices work together in a practical program.
Key Takeaways
- A map stores key-value pairs rather than a sequence of elements.
- Map declarations use the form map[keyType]valueType.
- make creates an initialized map that can accept new entries.
- Reading a missing key returns the zero value of the map’s value type.
- Reading from a nil map is valid, but writing to one causes a runtime panic.
- Assigning a map to another variable does not create an independent copy of its entries.
- Map keys must be comparable, which means slices cannot be used as map keys.
- The comma-ok idiom lets us check whether a key exists.
- Maps are useful for tasks such as word counting, while slices are useful for storing and sorting sequences of results.
Conclusion
Maps are one of the most useful built-in data structures in Go.
Once I understood how keys and values work, how nil maps differ from initialized maps, and how the comma-ok idiom helps check whether an entry exists, maps became much easier to use.
The word-counting example also shows how maps can simplify a common programming task. Instead of manually searching through a sequence every time we encounter a word, we can use the word as a key and update its count.
Combined with slices, maps give us a flexible way to store, process, and organize data in Go.
Have you used maps to solve a problem in Go? Share your experience or questions in the comments!
#Go #Golang #Programming #LearnGo #MapsInGo #SoftwareDevelopment #Coding