Languages Go Maps: A Simple, Flexible Data Structure

Go Maps: A Simple, Flexible Data Structure

In the Go language, a map is a reference to a hash table with key value pairs. It is one of the most versatile of all data structures found in the Go standard library. Here we’ll discuss maps and some of their intricacies, with code examples.

Go maps explained

Go maintains a small range of collection types in the standard library. This is a sharp contrast to the massive libraries that we find in many programming languages. The map is one such collection type, and it hosts an unordered collection of key-value pairs. Other names of this data structure are hash table, hash map, dictionary, unordered map, or associative array.

The C standard library does not have a map data type, but in Go map is part of the standard library. Go maps are similar to the std::map type that we find in C++. There is no theoretical limit to its capacity or the amount of data it can hold. The keys represent a unique element of the type that supports the use of operators, such as == (equality) and != (not equal). As a result, most of the Go built-in types (such as, int, float64, rune, string, most arrays and structs, custom types, pointers, etc.) can be used as keys. But note that any non-comparable types like arrays of structs (that do not support == and != operator) should not be used as map keys. However, there is no restriction of type as such for the value counterpart in the map. It may be pointers, reference type, built-in or custom type.

Map lookups are way faster than linear search, except that direct indexing can be made to the arrays or slices, which undoubtedly have faster access. But in general the lookup performance of maps is very efficient. Slices cannot be used as map keys but we can tweak them slightly to our advantage to accommodate any type with the help of the conversion technique. For example, we may convert []bytes slices into strings and use as map keys and reconvert them into []bytes as per our requirement. All the keys in the map must be of the same type and same with the values, but the types can differ between keys and values.

Interestingly, if we store a map key as an empty interface, interface{}, we can practically store any type of value. Between key-value pairs, the rules for value are much simpler but key is a bit tricky and has certain stringent rules. Although we can tweak it sometimes it is not only advisable but also common sense to keep the type of key within the norms of simple built-in types.

Working with Go maps

Like other Go reference types, map is also created with the make() built-in function. The first argument of this function is the map type. The second argument is optional but if specified it refers to the initial capacity of the map. We may ignore the second argument quite safely most of the time because maps dynamically resize themselves as we go on adding elements. Also there is no harm in providing initial capacity either. Here is an example.


package main

func main() {
//creating a map
scoreMap := make(map[string]float64)
//with initial capacity
//scoreMap := make(map[string]float64, 0)

//populating map
scoreMap["Ben"] = 6.89
scoreMap["Arun"] = 9.76
scoreMap["Vinod"] = 7.59
scoreMap["Roger"] = 8.36
scoreMap["Paul"] = 8.95

fmt.Println(scoreMap)

//accessing individual element with key

fmt.Println(scoreMap["Vinod"])

//iterating through and printing key-value pairs
for name, score := range scoreMap {
	fmt.Printf("%st%vn", name, score)
}
}

Output


map[Arun:9.76 Ben:6.89 Paul:8.95 Roger:8.36 Vinod:7.59]
7.59
Arun    9.76
Vinod   7.59
Roger   8.36
Paul    8.95
Ben     6.89

We must understand that there is a built-in new() function that may be misunderstood to be used to create maps. The difference is that the new() function returns a pointer to an uninitialized reference to a map. This simply means that it points to a nil map. Any attempt to manipulate it at run-time will result in an error. For example, the following code compiles fine but does not work as we try to populate it.


ptrToUnrefMap := new(map[int]string)
(*ptrToUnrefMap)[1] = "hello"

We can also create maps with initialized data using composite literal as follows.


mymap := map[string]int {
	"try again": 0,
	"good":   1,
	"better": 5,
	"best":   10,
}

Map Lookup intricacy

Map lookups are quite simple syntactically. If we want to look up a specific value we may use the index operator – [] and supply the key as follows.


val := mymap["better"]
fmt.Println("better = ", val)

The map is looked up with the matching keys and the corresponding value is returned. But what if the key is not present? In such a case zero value is returned to indicate that the matching key is not found. Note that in the example above, mymap map, there is actually a zero value present corresponding to the key “try again”. Therefore, if we receive a zero value in this case it is not clear whether the value returned is due to an unmatched key or an actual zero value in the map. For example, the following code will result in printing the same value 0.


val := mymap["try again"]
fmt.Println("try again = ", val)

//lookup with key that does not exists
val := mymap["again"]
fmt.Println("try again = ", val)

There is a way to frame map lookups in Go so that such ambiguity does not arise. Here is an example.

if val, ret := mymap[“try again”]; ret { fmt.Println(“value of try again is “, val) } else { fmt.Println(“Not found. invalid key”) }

We can also use a blank identifier to get a hint of the key present in the map or not.


key := "ty again"
_, ret := mymap[key]
fmt.Printf("is %q present in mymap? %tn", key, ret)

Manipulating maps

We can perform simple CRUD operations on the map. For example, we may insert new values, update and delete existing ones in the map as follows:


//insert
mymap["awesome"] = 15
fmt.Println(mymap)

//update
mymap["good"] = 2
fmt.Println(mymap)

//delete
delete(mymap, "try again")
fmt.Println(mymap)

Output


map[awesome:15 best:10 better:5 good:1 try again:0]
map[awesome:15 best:10 better:5 good:2 try again:0]
map[awesome:15 best:10 better:5 good:2]

The rules for insertion is that if the new key is not present then a new key-value pair is inserted, otherwise the existing matched key is updated with the new given value. Update is pretty straightforward like insert. If we try to update a key that is not present, a new key-value pair is inserted. In case of delete, if the key does not exist it does nothing, otherwise the matched key-value pair is removed from the map.

The key in the map cannot be changed but it is not very difficult to achieve a kind of similar effect.


oldKey := "try again"
newKey := "try harder"
v := mymap[oldKey]
delete(mymap, oldKey)
mymap[newKey] = v
fmt.Println(mymap)

Final thoughts

The map is the most simple yet flexible data structure found in Go standard library. The syntaxes are as simple as using an array or slices. The lookup performance and ease of using it in code is excellent. We can perform simple CRUD operations, iteration over the elements present in the map with absolute ease. We need not bother about the capacity of the map nor we need to give a value for a head start. The size gets increased as we go on inserting items and decreases as we delete items from the map.

Latest Posts

Related Stories