Golang composite data types – Maps

Go supports composite types such as array, slice, maps and structures. We discussed about Arrays and Slices in the previous blog, now is the time to go through the concept of maps in Golang.

Golang Maps

One of the most useful data structures in computer science is the hash table. Almost every language provides a hash table implementation, that offers fast lookups, adds, and deletes. Go provides a built-in map type that implements a hash table and are suitably termed as Maps. Like in other languages where we have dictionaries (Python) or hash maps (Java), Maps are Golang’s built in associative data types where every value is associated with a key. Let us learn more about Maps in Golang in the way we best know – yes, by writing code!

Declaring and Initializing Maps

Maps in Golang can be visualized in this way => map[keyType]valueType so a map where we want to store the scores of students by their names (which has strings as keys and integers as values) would be written like scores[string]int.

A map can be initialized in two different ways:

  1. var mscores := make(map[string]int)
  2. var score map[string]int

The first option uses make(map[keyType]valueType) function available to us from Go standard library to initialize a map, but you could argue and say why can’t it be simple enough to create a map this way: var score map[string]int? Let’s look at the difference between the two.

make, allocates and initializes a hash data structure and it returns a value mscores that points to the newly created structure. But in the later case, a new hash structure is not initialized, so the value of score is nil. Map types are reference types, like pointers or slices, and so the value of m above is nil. Adding data to this map will result in runtime errors as we will see in the example.

package main
import (
"fmt"
)
func main() {
var mscores = make(map[string]int)
mscores["Chetan"] = 90
// Returns mscrores: map[Chetan:90]
fmt.Println("mscrores:", mscores)
var score map[string]int
// Runtime error: panic: assignment to entry in nil map
score["Chetan"] = 90
var students = make(map[string]int)
students["Chetan"] = 90
students["John"] = 75
students["Alice"] = 30
// Returns mscrores: map[John:75 Alice:30 Chetan:90]
fmt.Println("students scrores:", students)
values := map[string]int{
"abc": 123,
"def": 345,
"ghi": 567,
"jkl": 897,
}
// Returns values: map[abc:123 def:345 ghi:567 jkl:897]
fmt.Println("values:", values)
}

view raw
declare_maps.go
hosted with ❤ by GitHub

 

As you can see in the above code snippet, there are multiple ways to initialize maps with keys and values. You can either individually assign keys with values or you could initialize all values in a single statement

Assigning individual keys:

var mscores = make(map[string]int)

mscores["Chetan"] = 90

Initialize all keys of a Map:

values := map[string]int{
       "abc": 123,
       "def": 345,
       "ghi": 567,
       "jkl": 897,
   }

Cool, now that we have the Maps ready, let’s look at all the operations we can perform with them.

Operations with Maps

Insert and Retrieve: Adding elements to maps and retrieving keys from map is very trivial and intuitive. In the below example, we can set the scores for Alice, John using their names as keys and setting the values with their scores with this code: mscores[“Alice”]  = 30

To retrieve the score for a given student, we can return the value with the index like this: var aliceScore := mscores[“Alice”]

Iterate over map elements: We could also retrieve all the keys and values in a map using the for loop using the helper function range. Note that the order of iteration will never be the same (as probably with previous versions of Go). But if your specific use case depends on maintaining orders, you could sort the map based on the keys and retrieve the elements and work on them.

Zero values: Now what happens when we try to retrieve John’s score? As we haven’t added any value for John, Golang will return the zero value for the key, so the statement mscores[“Bob”]  will print 0

Length of map: We can easily figure out the number of keys that are present in a map using the built in function len(map). For instance, if we would like to count the number of students that are stored in our students map, using len() is the way to go about it.

package main
import "fmt"
func main() {
var mscores = make(map[string]int)
mscores["Chetan"] = 90
mscores["John"] = 75
mscores["Alice"] = 30
// Returns Alice score: 30
fmt.Println("Alice score:", mscores["Alice"])
// Iterating over contents of a map, code returns
// Key: Alice Value: 30
// Key: Chetan Value: 90
// Key: John Value: 75
for key, value := range mscores {
fmt.Println("Key:", key, "Value:", value)
}
// Returns Bob's score: 0 (uses the zero value)
fmt.Println("Bob's score:", mscores["Bob"])
// Returns Length of mscores map: 3
fmt.Println("Length of mscores map:", len(mscores))
// Checks if the key exists in a map
// i returns the value for the key if exists or returns the zero value
// ok returns bool value indicating if the key exists
i, ok := mscores["Bob"]
// Returns i, ok 0 false
// As the key Bob doesnt exist, i returns zero value 0
// and ok returns false which indicates non existence of key
fmt.Println("i, ok", i, ok)
// Delete element from the map
delete(mscores, "Alice")
// Returns mscores map[John:75 Chetan:90]
// Alice key is removed from the map
fmt.Println("mscores", mscores)
}

view raw
working_with_maps.go
hosted with ❤ by GitHub

Existence check: More often than not, we need to check a key is part of the map we are working on. For example, if student Bob does exist in the map before we even start working on it? Golang handles this check for us.

When we try to access an element, Golang returns two values:

  1. a boolean value that indicates if the key exists in the map and if not
  2. it also returns the zero value as we saw in the Bob’s case

In our example, since the key “Bob” does not exist in mscores map, the variable ok returns false.

Deleting elements: Go also provides a built in function delete(map, key) that takes in the key and deletes it from the map. So if at run time we want to delete a student from the map, we can do using the statement: delete(mscores, “Alice”). In our code snippet, when we try to print all elements of the map mscores, the key “Alice” is removed and the only keys available are “John” and “Chetan”.

Maps as reference types

Like slices, map types in Golang are also reference types, which means if map is assigned to a new variable, then a change in map will change the values for both the variables. This is because both the variable point to the same hashmap data structure. For instance, in the below example, the new variable (newVeggies) also points to the same map (veggies), so when we change the value of orka key in the veggies map, they reflect in the newVeggies as well.

package main
import "fmt"
// Function tp update maps
func updateNumbers(m map[string]int) {
m["three"] = 3
m["four"] = 4
}
func main() {
veggies := map[string]int{
"onion": 12,
"cabbage": 15,
}
veggies["orka"] = 90
// Returns Original veggies map map[onion:12 cabbage:15 orka:90]
fmt.Println("Original veggies map", veggies)
newVeggies := veggies
newVeggies["orka"] = 180
// Returns New Veggies map map[cabbage:15 orka:180 onion:12]
fmt.Println("New Veggies map", newVeggies)
// Returns Updated veggies map[onion:12 cabbage:15 orka:180]
fmt.Println("Updated veggies", veggies)
// Passing map to a function
numbers := make(map[string]int)
numbers["one"] = 1
numbers["two"] = 2
// Returns numbers map[one:1 two:2]
fmt.Println("numbers", numbers)
// Add elements to the map in the function
updateNumbers(numbers)
// Returns after numbers are updated map[one:1 two:2 three:3 four:4]
fmt.Println("after numbers are updated", numbers)
}

Passing maps to the functions: Maps can also be passed to the functions and yes as you guessed it, they are passed as references. In the above example, when we add more elements to the map numbers in updateNumbers() function, the map numbers is also changed in the main function.

Deep Copy

As we noticed in the previous section, maps are reference types, so the changes that happened to the map in the function got propagated over to the main function. But we can easily avoid this situation by copying the map so that the original map is always maintained. We achieve by performing a deep copy of the map.

package main
import (
"encoding/json"
"errors"
"fmt"
)
func deepCopyMap(src map[string]int, dst map[string]int) error {
if src == nil {
return errors.New("src cannot be nil")
}
if dst == nil {
return errors.New("dst cannot be nil")
}
jsonStr, err := json.Marshal(src)
if err != nil {
return err
}
err = json.Unmarshal(jsonStr, &dst)
if err != nil {
return err
}
return nil
}
func main() {
scores := map[string]int{"Alice": 90, "Bob": 100}
// Returns Scores: map[Alice:90 Bob:100]
fmt.Println("Scores:", scores)
dstScores := make(map[string]int)
deepCopyMap(scores, dstScores)
dstScores["Celine"] = 110
// Returns Scores: map[Alice:90 Bob:100]
fmt.Println("Scores:", scores)
// Returns Dst Scores: map[Alice:90 Bob:100 Celine:110]
fmt.Println("Dst Scores:", dstScores)
}

view raw
deep_copy_maps.go
hosted with ❤ by GitHub

As you can see in the above code snippet, the value of scores has not changed even when we add a new element in dstScores map. So these two maps are now independent of each other and can be worked upon.

Nested Maps

More often than not, our data is going to be more complex or involved that the examples we have seen so far. Imagine a map shoppingList that contains details of the fruits, vegetables and other necessary items that need to be bought from a local grocery shop.

Now in this example, you would need to not only store different types of items but there can be subcategories for every item type. For instance, veggies internally will have onion, orka and they need to be bought in the quantities of 2kg and 3kg respectively. How will we store the shoppingList then?

So at the first level, the shoppingList map will look like

shoppingList := make map[string]int{“veggies”:2, “fruits”:3, “other”:6}. This map will tell us we need to purchase 2 veggies and 3 different types of fruits. But to store the details of which veggies to be bought and in what quantity, we need to expand the map. We do this by nesting of maps, so the first level map will store the category of grocery items we need to purchase (like veggies and fruits) while the associated nested map contains the details for every category (like onion 2kgs and 12 bananas).

package main
import "fmt"
func main() {
// shoppingList is a map that has a map inside it
shoppingList := make(map[string]map[string]int)
// veggies key points to veggiesMap
veggiesMap := map[string]int{"onion": 2, "orka": 3}
shoppingList["veggies"] = veggiesMap
// fruits key points to fruitsMap
fruitsMap := map[string]int{"banana": 12, "apples": 5, "oranges": 3}
shoppingList["fruits"] = fruitsMap
// Returns Shopping list categories:
// Category: veggies
// Category Details: map[onion:2 orka:3]
// Category: fruits
// Category Details: map[banana:12 apples:5 oranges:3]
fmt.Println("Shopping list categories:")
for key := range shoppingList {
fmt.Println("Category:", key)
fmt.Println("Category Details:", shoppingList[key])
}
}

view raw
nested_maps.go
hosted with ❤ by GitHub

In the above example, veggies and fruits are the top level categories of our shopping list and the map inside veggies and fruits keys, store details about what items to purchase and in what quantities.

Concurrency and Maps

You must have heard or experienced that maps in Golang are not safe for concurrent use. So what does this mean?

In case where multi goroutines are trying to read from or write into maps simultaneously ( or concurrently ), there could be cases of stale data being read or in case of uncontrolled access, it can lead to runtime crash or panic. Problems typically occur during updates and not if goroutines are trying to just lookup data from maps. Essentially operations on maps are not atomic in Go and this is not great.

To prevent this problem, we will need to synchronize access to the maps which can be achieved with mutexes. Some of the possible implementations may involving using:

  1. sync.RWMutex – reader/writer mutual exclusion lock
  2. atomic.AddUint32 –  provides low-level atomic memory primitives useful for implementing synchronization algorithms.
  3. sync.Map – map that is safe for concurrent use by multiple goroutines

Hope you are confident using Maps in Golang, feel free to comment or post suggestions. Here are a few resources on maps that would be useful to you, see you all soon 🙂

Good Reads

  1. Go by example: https://gobyexample.com/maps
  2. Go blog: https://blog.golang.org/go-maps-in-action
  3. Golang.org: https://golang.org/doc/faq#atomic_maps

 

 

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.