Today we continue our discussion about maps. In this topic, we will learn how to pass an estimate for the initial capacity of a map, check the length of a map, and delete items (key-value pairs) from a map. We will also look at how to iterate over all key-value pairs in a map with the help of the for...range loop and how to implement a new data type that is not native to Go with the help of maps.
Allocating capacity and checking the map length
To start learning about additional operations that we can perform with maps, first we need to declare and initialize one. Let's go ahead and create the elements map via the make function and add one additional argument known as hint:
elements := make(map[string]string, 3) // the initial capacity of 'elements' is 3
As you can see in the above declaration, the hint argument allows us to pass the make function an estimate of the initial capacity of the map.
hint argument, we can pre-allocate memory for at least hint amount of entries in the map, which results in a subtle increase in run-time performance.Now that we have declared and initialized the elements map, let's add some items or key-value pairs to it:
elements["H"] = "Hydrogen"
elements["He"] = "Helium"
elements["Li"] = "Lithium"
Once we have added items to our map, we can work with its content in different ways. Let's start by checking the length of the map via the len function in the builtin package. This will help us learn how many key-value pairs there are in our map:
fmt.Println("element length is:", len(elements)) // element length is: 3
Although we have previously passed the argument hint with the capacity of 3 when declaring the elements map, we could add more elements to this map without any problems. Because, as we've mentioned, Go maps are mutable and they will grow on-demand, no matter which initialization method we choose:
elements["Be"] = "Beryllium"
elements["B"] = "Boron"
fmt.Println("element length is:", len(elements)) // element length is: 5Deleting items from a map
We can delete items from a map with the help of the delete function from Go's builtin package:
delete(elements, "H")
fmt.Println(elements) // map[B:Boron Be:Beryllium He:Helium Li:Lithium]
The delete function works as follows: it takes the name of the map as its first argument and a specified key from the same map as its second argument. In the above example, we pass the map elements and the key H, thus deleting the H:Hydrogen key/value pair from the elements map.
delete function is that if we try to delete a key that is not present in the map, nothing will be deleted and there will be no runtime error.Finally, if we tried to check the elements map length after deleting the H key, we would get the following output:
fmt.Println("element length is:", len(elements)) // element length is: 4Iterating over all items in a map
We can iterate over all the items in a map using the for...range loop. However, since a map is an unordered collection of keys and values, we never know exactly in what order the map's contents will be organized.
We'll start off by implementing a for...range loop to iterate over the keys of the elements map:
for key := range elements {
fmt.Println(key)
}
// Output:
// B
// Li
// He
// Bo
We can also iterate over key-value pairs at the same time! Let's create a new map movieRatings that will contain a movie's name as a key and a movie's RottenTomatoes rating as its integer-type value; then we'll implement a for...range loop to iterate over the map's key-value pairs:
movieRatings := map[string]int{
"The Matrix": 88,
"Speed": 94,
"The Matrix Reloaded": 73,
"John Wick": 86,
}
// Option #1 - create the 'val' variable to print the values of the map
for key, val := range movieRatings {
fmt.Println(key, val)
}
// Output:
// The Matrix 88
// Speed 94
// The Matrix Reloaded 73
// John Wick 86
Another way to print key-value pairs at the same time is to pass the key variable within square brackets [] after the map's name within the for...range loop:
// Option #2 - pass the 'key' variable within [] square brackets after the map's name
for key := range movieRatings {
fmt.Println(key, movieRatings[key])
}
If you try to run the above code multiple times, you will probably get the key-value pairs of movie names and ratings in a different order; this happens because maps are unordered collections.
Modifying map values during iteration
Now, let's consider a scenario where you want to iterate over the map and modify its contents. In Go, map values are not addressable, meaning map values can't be modified directly during iteration.
However, we can overcome this by using the map key to update the value (val):
// Increase the ratings in the 'movieRatings' map by 5
for key, val := range movieRatings {
movieRatings[key] = val + 5
}
for key, val := range movieRatings {
fmt.Println(key, val)
}
// Output:
// The Matrix 93
// Speed 99
// The Matrix Reloaded 78
// John Wick 91
In the above example, we iterate over the movieRatings map, and for each key-value pair, we update the value (val) of the map item by increasing the original rating by 5 using the map key.
However, the situation changes when the map values are data types with an underlying data structure, such as slices, arrays, or other maps. In these cases, iterating over the map with a for...range loop gives us a copy of the value. Modifying this copy doesn't affect the original data stored on the map.
To affect the original data, we need to use the map key to access and modify it:
movieCharacters := map[string][]string{
"Neo": {"Noodles", "Sushi"},
"John Wick": {"Steak", "Bacon"},
}
// Attempting to modify the slices directly during iteration won't work
for _, foods := range movieCharacters {
foods = append(foods, "Pizza") // This modifies a copy of the slice, not the original
}
fmt.Println(movieCharacters) // map[Neo:[Noodles Sushi] John Wick:[Steak Bacon]]
// To modify the original slices, update them through the map key
for key := range movieCharacters {
movieCharacters[key] = append(movieCharacters[key], "Pizza")
}
fmt.Println(movieCharacters) // map[Neo:[Noodles Sushi Pizza] John Wick:[Steak Bacon Pizza]]
The above code snippet highlights an essential concept in Go: when the map's values are data types with an underlying structure (like slices, arrays, or other maps), modifications made directly during iteration will NOT affect the original data. Instead, you must use the map key to make effective modifications.
Implementing maps to create a set
A set is an abstract data type that can store unique elements without any particular order. Since sets can't have multiple occurrences of the same element, it makes them a highly efficient data type to perform logical operations such as unions or intersections.
Although Go doesn't have sets natively, we can implement our own set data type with the help of maps. Let's take a look at the most basic implementation of a set, and create a map with the string type for keys and the bool type for values:
vegetables := map[string]bool{
"🥕": true,
"🧅": true,
"🥦": true,
}
After creating the vegetables set, we can perform some basic operations such as checking if an element exists within the set:
...
if _, ok := vegetables["🥕"]; ok {
fmt.Println("🥕 is in the set.")
}
if _, ok := vegetables["🍇"]; ok {
fmt.Println("🍇 is in the set.")
}
// Output:
// 🥕 is in the set
Another way to create a set is by using an empty struct type for values:
fruits := map[string]struct{}{
"🍎": struct{}{},
"🍊": struct{}{},
"🥝": struct{}{},
}
In very simple terms, the difference between using bool or empty struct type for values when creating a set is that an empty struct will take 0 bytes of memory, in contrast to 1 byte that a bool type will require. This might not look like a big deal with small sets such as in the previous examples, however, when working with big sets, it is more efficient to use an empty struct as the value type, because it will take up less memory, and also the processing time will be much faster.
If you want to know more about the performance gain when using empty struct types as values for maps, you can take a look at this article. It shows that if a map has at least 100 items, an empty struct type as a map's value is about 47.76% faster and consumes significantly less memory than empty interface{} type values.
Summary
In this topic, we've learned additional operations with maps, such as:
- Passing an estimate of the initial capacity of a map to the
hintargument of themakefunction; - Checking the length of the map via the
lenfunction; - Deleting key-value pairs from a map by passing both the name of the map and the key as arguments to the
deletefunction; - Different ways of iterating over all items in a map using the
for...rangeloop.
We've also learned how to create a new data type called set with the help of maps by using bool or empty struct types as the map's values. Sets are data types that do not allow multiple occurrences of the same element and can be useful for many logical operations.
Now, to make sure you remember all this information and can implement it, let's answer a couple of questions and do some exercises. Good job!