Introduction to Maps

A map is a built in Go data structure that stores information as key value pairs.

Unlike arrays and slices, where elements are accessed using their positions (indexes), values in a map are accessed using their keys.

Every entry in a Go map consists of two things:

  • Key: An identifier used to find an entry in the map.
  • Value: The information associated with that key.

For example, imagine storing information about a user. We could use the user's name and email as key value pairs.

user := map[string]string{ "name": "Adetola", "email": "adetola@yopmail.com", } 

syntax

The general syntax for declaring a map is:

map[keyType]valueType 

In our example:

map[string]string 
  • map tells Go that we are declaring a map.
  • The first string specifies the data type of the keys.
  • The second string specifies the data type of the values.

This means we have a map where both the keys and values are strings.

Can keys and values have different data types?

Yes. The key and value types do not have to be the same.

ages := map[string]int{ "Adetola": 25, "Tobi": 30, } 

Here, the keys are strings, while the values are integers.

Other possible combinations include:

map[int]string map[string]bool map[string]float64 

However, all keys in a particular map must have the same type, and all values must have the same type.

Key takeaway: A Go map stores data in key-value pairs. Unlike arrays and slices, we use keys rather than indexes to access its values. The key and value types can be different, but each must remain consistent throughout the map.

Declaring and Initialising Maps

There are several ways to create a map in Go.

1. Using a map literal

A map literal is useful when you already know the entries you want to store. The map is created and populated with values immediately.

userAges := map[string]int{ "Ade": 25, "Tola": 30, } 

Here:

  • userAges is the variable name.
  • map[string]int declares a map with string keys and integer values.
  • "Ade" and "Tola" are the keys.
  • 25 and 30 are their corresponding values.

A map literal allows us to create and populate a map in one step.

2. Using make()

The built-in make() function is used when you want to create an empty map and add entries later.

userAges := make(map[string]int) 

This creates an initialized, empty map that is ready to receive entries.

userAges := make(map[string]int) userAges["Ade"] = 25 userAges["Tola"] = 30 fmt.Println(userAges) 

Output:

map[Ade:25 Tola:30] 

You can also provide an optional initial capacity hint:

userAges := make(map[string]int, 10) 

This tells Go that you expect the map to hold approximately 10 entries. It does not limit the map to 10 entries.

3. Declaring a map without initialising it

You can declare a map without initialising it.

var userAges map[string]int 

This creates a nil map. A nil map can be read from, and its length can be checked, but you cannot add entries to it.

var userAges map[string]int fmt.Println(userAges["Ade"]) // 0 fmt.Println(len(userAges)) // 0 

However, attempting to add an entry causes a runtime panic.

var userAges map[string]int userAges["Ade"] = 25 // Runtime panic 

Why?

Because the map has been declared but not initialised. It does not have an underlying map structure ready to store entries. To add entries, initialise it first using either a map literal or make().

Key distinction: var userAges map[string]int creates a nil map, while make(map[string]int) creates an initialized, empty map.

Both initially have zero entries, but only the initialized map can accept new entries.

Adding and Updating Values in a Map

Go uses the same syntax to add a new entry and update an existing entry.

The general syntax is:

mapName[key] = value 

1. Adding a new entry

When the key does not already exist, Go creates a new entry.

userAges := make(map[string]int) userAges["Ade"] = 25 userAges["Tola"] = 30 fmt.Println(userAges) 

Output:

map[Ade:25 Tola:30] 

The first assignment creates an entry with the key "Ade" and value 25 and the second assignment creates another entry with the key "Tola" and value 30.

2. Updating an existing entry

When the key already exists, assigning a new value updates the existing entry.

userAges := map[string]int{ "Ade": 25, "Tola": 30, } userAges["Ade"] = 26 fmt.Println(userAges) 

Output:

map[Ade:26 Tola:30] 

Notice that "Ade" originally had a value of 25. After the assignment, its value becomes 26.

Go does not create another "Ade" entry. It updates the existing one.

Key takeaway: The assignment syntax is the same for adding and updating. Go checks whether the key already exists and either creates a new entry or replaces the existing value.

Accessing Values in a Map

To retrieve a value from a map, you use its key. The syntax is:

mapName[key] userAges := map[string]int{ "Ade": 25, "Tola": 30, } fmt.Println(userAges["Ade"]) 

Output:

25 

Go looks for the key "Ade" and returns the value associated with it.

What happens when a key does not exist?

If you try to access a key that does not exist, Go returns the zero value of the map's value type.

For example:

userAges := map[string]int{ "Ade": 25, } fmt.Println(userAges["Tola"]) 

Output:

0 

Because the value type is int, Go returns the integer zero.

Other examples:

Map type Zero value for a missing key
map[string]int 0
map[string]string ""
map[string]bool false
map[string]float64 0.0

However, this creates a problem like what if a key does not exist, but another key exists with an actual value of 0?, How do we distinguish between the two?

Go solves this using the comma-ok idiom.

Checking Whether a Key Exists (Comma-OK Idiom)

Go allows you to retrieve a value and check whether its key exists at the same time.

Example:

userAges := map[string]int{ "Ade": 25, "Tola": 30, } age, exists := userAges["Ade"] if exists { fmt.Println("Age:", age) } else { fmt.Println("User not found") } 

Output:

Age: 25 

Let's break this down:

age, exists := userAges["Ade"] 

Go returns two values:

  1. age: The value associated with the key.
  2. exists: A boolean indicating whether the key exists in the map.

If the key exists, exists is true.

If the key does not exist, exists is false.

Let's look at another example:

age, exists := userAges["Femi"] if exists { fmt.Println("Age:", age) } else { fmt.Println("User not found") } 

Output:

User not found 

Even though Go returns the zero value for a missing key, the exists variable tells us that the key was not found.

This is particularly useful when working with user records, configuration, permissions, and other data where knowing whether an entry exists matters.

It is called the comma-ok idiom because of the two-value assignment used to check the result.

Deleting Entries from a Map

Go has a built-in function called delete() for removing entries from a map.

Syntax:

delete(mapName, key) userAges := map[string]int{ "Ade": 25, "Tola": 30, "Femi": 28, } delete(userAges, "Tola") fmt.Println(userAges) 

Output:

map[Ade:25 Femi:28] 

The delete() function removes the entry associated with the specified key.

A few things to remember:

  • Deleting an existing key removes its entry.
  • Deleting a key that does not exist does nothing.
  • delete() does not return a value.
  • Calling delete() on a nil map is safe and does nothing.

Getting the Number of Entries in a Map

You can use the built-in len() function to find out how many entries a map contains.

Example:

userAges := map[string]int{ "Ade": 26, "Tola": 30, "Femi": 28, } fmt.Println(len(userAges)) 

Output:

3 

The len() function returns the number of key-value pairs currently stored in the map.

Unlike an array, a map does not have a fixed number of entries. Its length changes as you add or delete entries.

For example:

userAges := make(map[string]int) fmt.Println(len(userAges)) // 0 userAges["Ade"] = 26 userAges["Tola"] = 30 fmt.Println(len(userAges)) // 2 delete(userAges, "Ade") fmt.Println(len(userAges)) // 1 

Iterating Over a Map

Sometimes you don't want to retrieve just one value. You want to go through every entry in a map.

Go uses the range keyword to iterate over maps.

userAges := map[string]int{ "Ade": 25, "Tola": 30, "Femi": 28, } for name, age := range userAges { fmt.Println(name, age) } 

Possible output:

Ade 25 Tola 30 Femi 28 

The output order may be different when you run the program.

for name, age := range userAges { 
  • for starts the loop.
  • range allows us to iterate over the map.
  • userAges is the map we want to iterate through.
  • name receives the key from each entry.
  • age receives the corresponding value.

For every entry in the map, Go assigns its key to name and its value to age.

The loop then executes the code inside its body.

fmt.Println(name, age) 

This prints the key and its corresponding value.

Important: Map iteration order

Go does not guarantee the order in which map entries are returned during iteration. This means you should not rely on a map to preserve the order in which entries were inserted.
If you need a specific order, you must collect the keys and sort them before iterating.

Important Characteristics of Go Maps

A. Map keys must be comparable

Go requires map keys to be types that can be compared using the == operator.

Common valid key types include:

  • Strings
  • Integers
  • Floating-point numbers
  • Booleans
  • Pointers
  • Arrays whose element types are comparable
  • Structs whose fields are all comparable

Slices, maps, and functions cannot be used directly as map keys.

For example:

// Valid ages := map[string]int{} // Invalid // users := map[[]int]string{} 

Why? Because slices cannot be compared using == in Go.

This restriction exists because Go needs to be able to compare keys when storing and retrieving map entries.

B. Maps are reference like types

When you assign a map to another variable, Go does not create an independent copy of all its entries.

Both variables refer to the same underlying map data.

Example:

first := map[string]int{ "Adetola": 25, } second := first second["Adetola"] = 30 fmt.Println(first["Adetola"]) // 30 fmt.Println(second["Adetola"]) // 30 

Notice that changing the value through second also affects what we see through first.

This is different from arrays, where assigning one array to another copies the entire array.

C. A map is not safe for concurrent writes by default

If multiple goroutines access a map concurrently and at least one of them writes to it, you need synchronization.

Ordinary Go maps are not automatically safe for concurrent writes.

We'll explore goroutines, concurrency, and synchronization later in your Go learning journey.

For now, remember that when multiple goroutines need to modify the same map, you must protect those operations appropriately.

Go Maps vs JavaScript Objects

Since you already know JavaScript, let's connect the concepts. In JavaScript, you might represent the same information using an object:

const userAges = { Adetola: 25, Tobi: 30, }; console.log(userAges.Adetola); 

In Go:

userAges := map[string]int{ "Adetola": 25, "Tobi": 30, } fmt.Println(userAges["Adetola"]) 

They can serve similar purposes, but they are not exactly the same.

Feature Go map JavaScript object
Structure Key-value pairs Properties and values
Key types One declared comparable type Strings or symbols
Value types One declared value type Can hold different types
Access users["Adetola"] users.Adetola
Add/update users["name"] = value users.name = value
Delete delete(users, "name") delete users.name
Missing key Returns zero value; comma-ok checks existence Returns undefined
Iteration for range for...in, Object.keys()

JavaScript also has a built-in Map

JavaScript's Map object is another useful comparison to Go maps.

const ages = new Map(); ages.set("Adetola", 25); ages.set("Tobi", 30); console.log(ages.get("Adetola")); 

JavaScript's Map supports keys of different types, while a Go map has one declared key type.

For example, JavaScript can do this:

const example = new Map(); example.set("name", "Adetola"); example.set(25, "age"); example.set(true, "active"); 

Go requires all keys in a particular map to have the same declared type.

Practical Example: User Lookup

Imagine you're building a backend service and need to look up a user's account balance using their user ID.

A map can help you associate each user ID with a corresponding balance.

balances := map[string]float64{ "USER-001": 15000.50, "USER-002": 25000.00, "USER-003": 8000.75, } balance, exists := balances["USER-001"] if exists { fmt.Println("Account balance:", balance) } else { fmt.Println("Account not found") } 

Output:

Account balance: 15000.5 

This is a practical use of maps: you have an identifier and need to retrieve its associated information.

Notice that:

  • The keys are strings because the user IDs are strings.
  • The values are float64 because the balances contain decimal values.
  • The comma-ok idiom checks whether the requested user ID exists.

This is a simplified example. In production financial systems, monetary values generally require more careful representation than ordinary floating-point numbers.

Lesson Takeaways

These are the points worth keeping in your study notes.

  1. A map is a built-in Go data structure that stores data as key-value pairs.
  2. A map is declared using map[keyType]valueType.
  3. Keys and values can have different types, but each type must remain consistent within its map.
  4. Map keys must be comparable. Slices, maps, and functions cannot be used directly as keys.
  5. You can initialise a map using a map literal or make().
  6. A nil map can be read from but cannot be used to add entries.
  7. Use mapName[key] = value to add or update an entry.
  8. Use mapName[key] to retrieve a value.
  9. A missing key returns the zero value of the map's value type.
  10. Use value, exists := mapName[key] to check whether a key exists. This is the comma-ok idiom.
  11. Use delete(mapName, key) to remove an entry.
  12. Use len(mapName) to get the number of entries.
  13. Use for range to iterate over a map.
  14. Map iteration order is not guaranteed.
  15. Assigning a map to another variable does not create an independent copy; both variables refer to the same map data.
  16. Ordinary Go maps are not safe for concurrent writes without synchronization.


  • map[string]int means string keys and integer values.
  • value, exists := myMap[key] lets you distinguish a missing key from a key containing a zero value.
  • Maps are useful when you need to retrieve information using an identifier rather than a numerical position.