The anatomy of maps in Go

A map is a composite data type that can hold data represented by key:value pairs.

What is a map?

A map is like an array except, instead of an integer index, you can have string or any other data types as a key. An illustration of a map looks like

{
  stringKey: intValue,
  stringKey: intValue
  ...
}

The syntax to define a map is

var myMap map[keyType]valueType

Where keyType is the data type of map keys while valueType is the data type of map values. A map is a composite data type because it is composed of primitive data types (see variables lesson for primitive data types).

Let’s declare a simple map.

package main

import "fmt"

func main() {
	var m map[string]int

	fmt.Println(m)
	fmt.Println("m == nil", m == nil)
}

// map[]
// m == nil true

In the above program, we have declared a map m which is empty, because zero-value of a map is nil. But the thing about nil map is, we can’t add values to it because like slices, map cannot hold any data, rather they reference the internal data structure that holds the data.

So, in the case of nil map, the internal data structure is missing and assigning any data to it will case runtime panic panic: assignment to entry in nil map. You can use nil map as a variable to store another non nil map.

Creating an empty map

An empty map is like an empty slice with internal data structure defined so that we can use it to store some data. Like slice, we can use make function to create an empty map.

m := make(map[keyType]valueType)

Let’s create a simple map age in which we can save the age of people.

package main

import "fmt"

func main() {
	age := make(map[string]int)
	age["mina"] = 28
	age["john"] = 32
	age["mike"] = 55
	fmt.Println("age of john", age["john"])
}

// age of john 32

In the above program, we have created an empty map age which holds int data and referenced by string keys. You can access or assign a value of a map using a key like map[key].

Using that information, we have assigned some age data of mina, john and mike. You can add as many values as you want, as map like slice can hold a variable number of elements.

Initializing a map

Instead of creating empty map and assigning new data, we can create a map with some initial data, like array and slice.

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}

	fmt.Println(age)
}

// map[john:32 mike:55 mina:28]

Accessing map data

In case array or slice, when you are trying to access out of index element (when the index does not exist), Go will throw an error. But not in case of map.

When you are trying to access the value by the key which is not present in the map, Go will not throw an error, instead, it will return zero value of valueType.

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}
	fmt.Println(age["mina"])
	fmt.Println(age["jessy"])
}

// 28
// 0

28 is correct because that’s the age of mina but since jessy is not in the map, Go will return 0 as zero-value of data type int is 0.

So, to check if a key exists in the map or not, Go provide another syntax that returns 2 values.

value, ok := m[key]

Let’s see the above example in a new syntax

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}
	minaAge, minaOk := age["mina"]
	jessyAge, jessyOk := age["jessy"]

	fmt.Println(minaAge, minaOk)
	fmt.Println(jessyAge, jessyOk)
}

// 28 true
// 0 false

So, we are getting extra information on whether a key exists or not. If the key exists, the second parameter will be true else it will be false.

Length of map

We can find out how many elements contained in a map using len function, which we saw in array and slice.

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}
	fmt.Println("len(age) =", len(age))
}

// len(age) = 3

💡 There is nothing like capacity in map because Go completely takes control of internal data structure of map. Hence don’t try to use cap function on map.

Delete map element

Unlike slice where you need to use a hack to delete an element, Go provide easier function delete to delete map element. Syntax of a delete function is as following.

func delete(m map[Type]Type1, key Type)

delete function demands the first argument to be a map and the second argument to be a key value.

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}

	delete(age, "john")
	delete(age, "jessy")

	fmt.Println(age)
}

// map[mike:55 mina:28]

💡 If the key does not exist in the map, like jessy in above example, Go will not throw an error while executing delete function.

Maps comparison

Like slice, a map can be only compared with nil. If you are thinking to iterate over a map and match each element, you are in grave trouble. But if you are in dire need to compare two maps, use reflect package’s DeepEqual function (https://golang.org/pkg/reflect/).

Map iteration

Since there are no index values in map, you can’t use simple for loop with incrementing index value until it hits the end. You need to use for range to do it.

package main

import "fmt"

func main() {
	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}

	for key, value := range age {
		fmt.Println(key, "=>", value)
	}
}

// john => 32
// mike => 55
// mina => 28

range in for loop will return key and value of map element. You can also use _ (blank identifier) to ignore either key or value in case you don’t need it, just like array and slice.

💡 Order of retrieval of elements in map is random when used in for iteration. Hence there is no guarantee that each time, they will be in order. That also explains why we can’t compare two maps.

Map with other data types

It’s not necessary that only string types should be the keys of a map. All comparable types such as boolean, integer, float, complex, string, etc. can also be keys. This should be very obvious, but boolean just blows my mind because boolean can only represent 2 values, either true or false. Let’s see what happens where we can use it.

package main

import "fmt"

func main() {
	age := map[bool]string{
		true:  "YES",
		false: "NO",
	}

	for key, value := range age {
		fmt.Println(key, "=>", value)
	}
}

// true => YES
// false => NO

I guess we found ourselves a use case for boolean key values. But what if we add duplicate keys.

package main

import "fmt"

func main() {
	age := map[bool]string{
		true:  "YES",
		false: "NO",
		true:  "YEAH",
	}

	for key, value := range age {
		fmt.Println(key, "=>", value)
	}
}

// ./prog.go:9:3: duplicate key true in map literal

This also proves that we are not allowed to add duplicate keys in a map.

Maps are referenced type

Like slice, map references an internal data structure. When you copy a map to a new map, the internal data structure is not copied, just referenced.

package main

import "fmt"

func main() {
	var ages map[string]int

	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}

	ages = age

	delete(ages, "john")

	fmt.Println("age", age)
	fmt.Println("ages", ages)
}

// age map[mike:55 mina:28]
// ages map[mike:55 mina:28]

As expected, ages map now has two elements because we deleted one element. Not only that, but we got the same change in age map as well. This proves that, like slice (but unlike an array), when you assign a variable with another map variable, they share the same internal structure.

To copy a map, you need to use for loop.

package main

import "fmt"

func main() {
	ages := make(map[string]int)

	age := map[string]int{
		"mina": 28,
		"john": 32,
		"mike": 55,
	}

	for key, value := range age {
		ages[key] = value
	}

	delete(ages, "john")

	fmt.Println("age", age)
	fmt.Println("ages", ages)
}

// age map[john:32 mike:55 mina:28]
// ages map[mike:55 mina:28]

In the above case, we haven’t copied the map but used map keys and values to store in a different map that implements its own underlying data structure.

💡 Since map references internal data structure, map passed as function parameter share same internal data structure just like slice. Hence, make sure to follow same guidelines as explained in the slices lesson.

#golang #maps