Maps are a built-in data structure in Go that act as a collection of key-value pairs. They are also known as associative arrays, hash tables, or dictionaries in other programming languages.
In this post, we will learn about maps in Golang, and how to use them.
We will learn about the syntax, how to create a map, how to add and delete elements from a map, how to iterate over a map, and how to check if a key exists in a map.
Creating a Map
When we want to create a new map, we need to declare its key and value types. The syntax for declaring a map type is:
var m map[keyType]valueType
This declares m
as a map with keys of type keyType
and values of type valueType
.
Note that there are some restrictions on the types that can be used as keys. However, any type can be used as a value.
This only declares the type, but to create an actual map, we need to use the built-in make
function.
For example, if we want to create a map with string
keys and int
values, we can do so as follows:
myMap := make(map[string]int)
This creates an empty map with string
keys and int
values.
Adding Key-Value Pairs to a Map
We can add key-value pairs to a map using the following syntax:
m[key] = value
This adds a key-value pair to the map m
. If the key already exists in the map, then the value is updated. If the key does not exist, then a new key-value pair is added to the map.
For example, if we want to add the key-value pair "the_answer": 42
to the map myMap
, we can do so as follows:
myMap["the_answer"] = 42
If you set a key that already exists, then it’s previous value is overwritten.
Getting a Value from a Map
We can get the value associated with a key from a map using the following syntax:
value = m[key]
This returns the value associated with the key key
in the map m
. If the key does not exist in the map, then the zero value of the value type is returned.
Let’s see an example of this. If we want to get the value associated with the key "the_answer"
from the map myMap
, we can do so as follows:
value := myMap["the_answer"]
However, if we try to get the value associated with the key "the_question"
from the map myMap
, we will get the zero value of the value type, which is 0
in this case.
value := myMap["the_question"]
// value == 0
Checking if a Key Exists in a Map
Since we get a zero value when a key doesn’t exist in a map, we can’t use this to determine if the key was present in the map or not. (What would we do if the key was intentionally set to the zero value?)
Fortunately, when obtaining a value from a map, we can also get a boolean value that indicates whether the key was present in the map or not:
value, ok := m[key]
The value of the second variable ok
is true
if the key was present in the map, and false
otherwise.
Deleting Key-Value Pairs
We can delete a key-value pair from a map using the inbuilt delete
function:
delete(m, key)
For example, if we want to delete the key-value pair "the_answer": 42
from the map myMap
, we can do so as follows:
delete(myMap, "the_answer")
The delete
function works even if the key doesn’t exist in the map, or even if the map is nil
. In that case, it does nothing, and acts as a no-op.
Iterating Over a Map
We can iterate over a map using the range
keyword. In each iteration, we can access a the key and value variables:
for key, value := range m {
// do something with key and value
}
Note that the order of iteration is not guaranteed (even for the same map in two different iterations). If you want to iterate over the map in a specific order, you will need to sort the keys first, or else maintain a separate slice of keys in the order you want.
Allowed Types for Keys
There are some restrictions on the types that can be used as keys in a map. The key type must be one of the following:
- Any type that is comparable using the
==
operator. This includes all the basic types, such asint
,string
,bool
, etc. and also pointers, channels and interfaces. - Any struct or array type that contains only types that are comparable using the
==
operator.
Slices, functions, and maps cannot be used as keys, since they are not comparable using the ==
operator.
Performance of Various Map Operations
After running some benchmarks on my machine, I found the following results:
Operation | Time Taken |
---|---|
Map Initialization | 3.874 ns/op |
Add Key-Value Pair (to an empty map) | 5.339 ns/op |
Get Value | 6.99 ns/op |
Update Value | 10.486 ns/op |
Delete Key-Value Pair | 12.71 ns/op |
Add Key-Value Pair (to a large map) | 123 ns/op |
Get Value (to a large map) | 7.6 ns/op |
Update Value (to a large map) | 8.8 ns/op |
Delete Key-Value Pair (to a large map) | 7.9 ns/op |
How we got these results
The complete code for the benchmarks is available on Github. When running this on a laptop, I got the following results:
=== RUN BenchmarkMapInit
BenchmarkMapInit
BenchmarkMapInit-8 310994732 3.874 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkMapAdd
BenchmarkMapAdd
BenchmarkMapAdd-8 130387846 9.213 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkMapGet
BenchmarkMapGet
BenchmarkMapGet-8 100000000 10.86 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkMapUpdate
BenchmarkMapUpdate
BenchmarkMapUpdate-8 82914201 14.36 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkMapDelete
BenchmarkMapDelete
BenchmarkMapDelete-8 71693688 16.58 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkLargeMapGet
BenchmarkLargeMapGet
BenchmarkLargeMapGet-8 156739308 7.662 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkLargeMapAdd
BenchmarkLargeMapAdd
BenchmarkLargeMapAdd-8 11410676 123.3 ns/op 63 B/op 0 allocs/op
=== RUN BenchmarkLargeMapUpdate
BenchmarkLargeMapUpdate
BenchmarkLargeMapUpdate-8 131575159 8.814 ns/op 0 B/op 0 allocs/op
=== RUN BenchmarkLargeMapDelete
BenchmarkLargeMapDelete
BenchmarkLargeMapDelete-8 153680978 7.911 ns/op 0 B/op 0 allocs/op
Now, each of the benchmarks for map operations involved a map initialization. For example, the benchmark for adding a key-value pair to a map involved initializing a map and then adding a key-value pair to it:
func BenchmarkMapAdd(b *testing.B) {
for i := 0; i < b.N; i++ {
m := make(map[int]int)
m[i] = i
}
}
So to get the final results, we need to subtract the time taken for map initialization from the time taken for the operation. For example, the time taken for adding a key-value pair to a map is 9.213 - 3.874 = 5.339 ns/op.
For large map operations, the map was initialized outside the test loop, so we don’t subtract the initialization time.
Note that the actual numbers are not important here. What is important is the relative performance of the various operations. For example, we can see that getting a value from a map doesn’t become that much slower even for a large map, but adding a key-value pair to a large map is much slower than adding a key-value pair to an empty map.