0% found this document useful (0 votes)
55 views15 pages

Maps - Practical Go Lessons-22

Maps are used to store key-value pairs where keys must be unique. A map is initialized using make or a map literal. It allows inserting, retrieving, and deleting elements by key in constant time. Keys have restrictions and must support equality checks, while values can be any type. Maps internally use a hash table for efficient lookup by mapping keys to indexes through hashing.

Uploaded by

prsnortin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
55 views15 pages

Maps - Practical Go Lessons-22

Maps are used to store key-value pairs where keys must be unique. A map is initialized using make or a map literal. It allows inserting, retrieving, and deleting elements by key in constant time. Keys have restrictions and must support equality checks, while values can be any type. Maps internally use a hash table for efficient lookup by mapping keys to indexes through hashing.

Uploaded by

prsnortin
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 15

Maps - Practical Go Lessons https://www.practical-go-lessons.

com/chap-22-maps

Chapter 22: Maps

1 What will you learn in this chapter?


• What is a map?

• What is a key, a value?

• How to create a map.

• How to insert an entry in a map.

• How to retrieve an entry from a map.

2 Technical concepts covered


• Map type

• Key-Value pair

• Map entry

• Hash table

• Time complexity

3 Why do we need maps?


In this section, we will detail how maps are working. But first, let’s take some time to understand why this data structure can be useful with
an example :

3.0.0.1 With a slice

1 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

// maps/without-maps/main.go
package main

import "fmt"

type testScore struct {


studentName string
score uint8
}

func main() {
results := []testScore{
{"John Doe", 20},
{"Patrick Indexing", 15},
//...
//...
{"Bob Ferring", 7},
{"Claire Novalingua", 8},
}
fmt.Println(results)
}

We have a type struct testScore and a slice results composed of testScore s elements. Now let’s imagine that I want to retrieve the
score of the student named Claire Novalingua.

We are using a slice we have to iterate over each element to find the item searched :

for _, result := range results {


if result.studentName == "Claire Novalingua" {
fmt.Println("Score Found:", result.score)
}
}

Why is this solution not optimal?

• We have to iterate potentially over all elements of the slice. Imagine that your slice contains thousands of elements! The impact on
performance can be important.

• The code written is not short. We use a for loop range and a nested comparison. Those five lines are not easy to read.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

4 What is a map?
A map is an unordered collection of elements of type T that are indexed by unique keys of type U1.

4.0.0.1 Example :

2 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

Map example[fig:Map-example]

In the previous figure (1) we have a map representing the football world cup winners by year. Here the key is the year (which is an uint8 )
and the values that represent the country name of the winner ( string ). The map type is denoted :

map[uint8]string

An element of a map is called a “map entry”. It’s also usually named a key-value pair.

4.0.0.2 General definition


map[keyType]elementType

With a map, you can do the following operations :

• store a value with a specific key

• delete a value stored with a specific key

• retrieve a value stored with a specific key

Let’s take another example; a dictionary can be stored using a map. In a dictionary, we have definitions of words that are stored. In this
case, the definitions are the elements, and the words represent the keys. When you use a dictionary, you search for a specific word to get
its definition. We never look into a dictionary by definition. This type of lookup might cost you a lot of time because definitions are not
indexed. We can keep this analogy for maps. We always make a lookup based on a specific key! Maps are indexed by keys.

Can we put all types defined in Go for the key type? And for the value type?

5 Keys: types allowed


You cannot use any type for keys of a map. There is a restriction. The type MUST : “The comparison operators == and != must be fully
defined for operands of the key type”2 Which types are therefore excluded ?

• function

• map

• slice

3 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

• array of function, map, or slice

• struct type that contains fields of type function, map, or slice

// FORBIDDEN: an array of slices


[3][]int

// FORBIDDEN : an array of functions


[3]func(http.ResponseWriter, *http.Request)

// FORBIDDEN: a type with a slice field


type Test struct {
scores []int
}
//...

6 Keys must be distinct


The keys of a map must be distinct.

If we use an image, a map is like a corridor with locked doors. Behind each door, there is a value. The keys that can open the doors are
unique (you can make copies of the keys, but the keys’ design stays the same). Each key opens a given door. There is a 1-1 relation
between the keys and the doors.

7 Elements
The elements are what you store on the map. For the elements, there are no restrictions concerning the type. You can store whatever you
want. You can also store another map into a value.

For instance, an element can be a year, the score of a match, a type struct representing a user of an application...

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

8 How to create a map


8.1 With the make builtin
You can use the make builtin to allocate and initialize a new map :

m:=make(map[string]int)

m will be a value of type map[string]int . This is called a map value, and internally it’s a pointer to a hash table. We will see in the next
sections what is exactly a hash table, so do not worry now about it.

8.2 With the “map literal” syntax


With the previous syntax, we initialize and allocate the map. But we do not fill it. We can fill it directly by using the map literal syntax:

worldCupWinners := map[int]string{
1930: "Uruguay",
1934: "Italy",
1938: "Italy",
1950: "Uruguay"}
fmt.Println(worldCupWinners)
//map[1930:Uruguay 1934:Italy 1938:Italy 1950:Uruguay]

In the previous code listing, we create a map named worldCupWinners . This map is directly populated with four entries. The first four
winners of the football world cup. The keys here are integers; they represent the years. The values are strings that represents the
country’s name that won the cup in the given year. In 1930 it was Uruguay that won the cup.

Please note that values can be repeated. The value Italy and Uruguay are repeated twice. It’s perfectly authorized.

Note also that after initializing a map, you can add new values to it. In our example, we can add another year to the map!

You can also use the map literal syntax to create an empty map.

a := map[int]string{}

4 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

In the previous code listing, a is a map (initialized and allocated), but no key-value pairs are stored in it.

9 What is a hash table?


Here is a simplified view of how a hash table works. (the go implementation is slightly different) :

Hash Table

A hash table is composed of 3 elements :

• A hash function. its role is to transform a key into a unique identifier. For instance, the key 1930 will be passed to the hash function,
and it will return “4”.

• An indexed storage that is used to keep the values in memory. The storage is eventually organized in buckets. Each bucket can store
a specific number of values.

When we add a key-value pair to the hash table, the algorithm will go through the following steps :

1. From the key get the return value of hash_function(key) (we denote the return value h ). h is the index where data is stored
(for instance, 4)

2. Store the value into the container at index h

Retrieving a value from a given key will also make use of the hash function :

1. From the value get the return value hash_function(key) . It will return the container index.

2. Extract the data from the given container and return it to the user.

9.1 A good hash function


A good hash function must have the following qualities :

• Avoid collisions of hashes :

◦ if you pass the key 1989 to the hash function, it will return, for instance i .
i will be the index of the storage of the value linked to 1989 .

◦ Imagine now that for 1938 the hash function returns the same index i !

◦ When you store something with the key 1989 it will erase what is already stored for the key 1938 .

◦ Imagine the mess that such collisions can produce! For instance, the hash function MD5 can produce collisions. (for more
information, read the article [@stevens2006fast])

• Compute an index to get the location of the data in a limited amount of time. (the hash function must be time-efficient)

5 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

• The hash produced must be stable in time. The key should produce the same hash at each call.

10 Hash table time complexity


• An algorithm’s complexity is the amount of resources it takes to run it on a machine.3

• The time complexity is a kind of complexity; it designates the amount of computer time needed to run a program4

Time complexity will depend on the hash table’s implementation, but keep in mind that time complexity is very low for searching a value
and inserting a new key-value pair.

The following time complexity applies in general for hash tables :

• Insertion : O(1)

• Search : O(1)

Search and insertion will take the same number of basic operations on a map containing three elements and a map containing 3 million
elements!

We say that it’s a constant-time algorithm. We also say that it’s order 1.I used here the Big-O notation5.

11 Go internals: The hash table implementation


This is an overview of how maps are implemented in Go. The internal implementation might change over time.

The source code is located in the runtime package (runtime/map.go).

• A Go map is an array of “buckets”

• A bucket contains a maximum number of 8 key/element pairs (also called 8 entries).

• Each bucket is identified by a number (an id).

11.0.0.1 Lookup an element


• To find an element into a map, the user will give a key.

◦ Key: 1930
• The key will be passed to the hash function it will return an hash value(which is an integer)

• This hash value contains the bucket id. The hash function does not directly return the id of the bucket, the return value h has to be
transformed to get the bucket id.

◦ Bucket id = 3
• Knowing the bucket id, the next step is to find the correct entry in the bucket. This is done by comparing the key given to all the
bucket keys.

◦ Key : “1930”. Go will iterate through the keys of the bucket and return the corresponding element

11.0.0.2 Insert an element


• The user provides the key and the element value

◦ Ex: Key : “1930” - Element : “Uruguay”


• The key is passed to the hash function.The hash function will return the hash.

• From the hash, we will retrieve the bucket id.

• Go will then iterate over the bucket elements to find a place to store the key and the element.

◦ When the key is already present, Go will override the element’s value.

6 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

Go Hashmap implementation

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

12 Example usage setup


In this section, we will take a look at the most common operations you can do on a map. To do that, we will use an example.

12.0.0.1 The example application


• You are asked to build an application for the HR department

• In the alpha version, we will load the list of employees via a CSV file

• The users will need to query employees by their employeeId (composed of letters and numbers)

◦ Ex: V45657 ,V45658...

Here is an excerpt of the CSV file :

employeeId,employeeName,genre,position
V45657,John Ollivero,M,CEO
V45658,Frane Elindo,F,CTO
V6555,Walter Van Der Bolstenberg,M,Sales Manager

12.0.0.2 Why maps?


The users will query an employee based on its unique Id.

• We will query employees based on a unique key

• This id is not an integer; we can use a slice or a map.

We will use a map, and we will create an employee type.

• Keys : the employeeId => string

7 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

• Elements : values of type employee

12.0.0.3 Reading data from CSV


Let’s build the first part of the script (to read the data into the file)

// maps/reading-csv/main.go
package main

import (
"encoding/csv"
"fmt"
"io"
"log"
"os"
)

func main() {
file, err := os.Open("/Users/maximilienandile/Documents/DEV/goBook/maps/usages/employees.csv")
if err != nil {
log.Fatalf("impossible to open file %s", err)
}

defer file.Close()

r := csv.NewReader(file)
for {
record, err := r.Read()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
fmt.Println(record)
}
}

The first step is to open the file employees.csv.

We are using the standard library os . Like always, we check for errors and return if they are some (but before returning, we are printing an
error message).

After that, we use the csv package. We create a reader with r := csv.NewReader(file) , that will allow us to read the file line by line. We
initialize a line counter to keep track of the line number.

Then we start the reading with the for loop. We read a new line with the record, err := r.Read() . The record variable is a slice of strings
( []string ). Next, we check for errors, with the subtility that r.Read() will populate err with io.EOF when it has reached the end of the
file. We have to check that before checking that err is not nil . If we have reached the end of the file, we will stop the for loop with the
keyword break . After that, we can finally read the data of the file.

The variable record will return, for instance [V45657 John Ollivero M CEO] .

The data is stored in a slice, and at the index 0, we will find the employeeID , at index one the name, at index two the genre, and the
position at index 3 !

We also have to define our type employee :

type employee struct {


name string
genre string
position string
}

The preparatory work is done let’s jump to the map creation and usage

13 Initialize and add a key/element pair

8 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

// initialize and allocate a new map


employees := make(map[string]employee)
// ...
employee := employee{
name: record[1],
genre: record[2],
position: record[3]}
// Add a new entry to the map
employees[employeeId] = employee

To add a pair composed of a key and a element simply use the following syntax :

m[key] = value

14 Retrieve a value
To get an element from a map you have to know it’s key. They are two different ways to do it :

14.0.1 Short syntax


Imagine that you are looking for the data related to employee number 3.

You will retrieve the value (a struct employee) by calling :

walter := employees["V6555"]

Here we assign to the variable walter the value contained into the map employeeMap with the key V6555.

14.0.2 When the key does not exists?


But what if the value does not exist? Will you make your program panic? Let’s take the risk :

// when there is no such pair


ghost := employees["ABC55555"]
fmt.Println(ghost)
//{ }
fmt.Println(reflect.TypeOf(ghost))
// main.employee

Here we attempt to get the value of the employee that has the id "ABC55555" .

The key does not exist on the map. Go will return the null value of the type.

14.0.2.1 Warning! Be very careful with this syntax because it can lead to errors.
In the case of our HR software example, imagine that after loading the data into the map, you propose to your users some kind of interface
where they can see the data of an employee in function of its id. What if the user types the id “100”. You implement a function that will
return an employee given a specific key. You will return an empty object employee.

We can guess that the employee does not exist, but it’s not 100% sure. Those empty fields can also come from a corrupted file.

That’s why Go creators have provided a more clever way to retrieve an entry in a map.

14.0.3 Two values assignment


The alternative syntax is the following :

v, ok := myMap[k]

The variable ok is a boolean that will hold the indication of the existence of the key-value pair in the map:

the key-value pair exists in the map, v is populated with the value at key k

the key-value pair does not exist, v is populated with the null value of type valueType

Often you will see this idiom :

9 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

// lookup with two values assignment


employeeABC2, ok := employees["ABC2"]
if ok {
// the key-element pair exists in the map
fmt.Println(employeeABC2)
} else {
fmt.Printf("No employee with ID 'ABC2'")
}

It’s possible to ignore the value if you just want to test the presence of a key into the map :

// ignore the value retrieved


_, ok := employees["ABC3"]
if ok {
// the key-element pair exists in the map
} else {
fmt.Printf("No employee with ID 'ABC3'")
}

In the previous example, we are telling the compiler that we do not need the value retrieved by using the underscore (_) character in the
assignation.

There is a shorter way to make the same operation :

// shorter code
if _, ok := employees["ABC4"]; ok {
// the key-element pair exists in the map
} else {
fmt.Println("No employee with ID 'ABC4'")
}

The two values assignment and the ok value check are done in one line!

14.0.4 Warning! Map values are not addressable


Values retrieved from a map are not addressable. You cannot print the memory address of a map value.

For instance, the following code :

fmt.Printf("address of the100 %p", &employeeMap[100])

Will result in a compiler error :

./main.go:66:14: cannot take the address of employeeMap[100]

Why this behavior? Because Go can change the memory location of a key-value pair when it adds a new key-value pair. Go will do this
under the hood to keep the complexity of retrieving a key-value pair at a constant level. As a consequence, the address can become invalid.
Go prefers to forbid the access of a possible invalid address than letting you try your chance. This is a good thing !

14.0.5 Memory Usage consideration


Please be aware that when you keep a value extracted from a map (and if you do not use the map anymore), Go will keep the whole map in
memory.

The garbage collector will not do its job and remove the unused memory.

15 Delete an entry
You can delete a key-value pair by using the delete built-in function. The function has the following header :

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

It takes:

• a map as first argument

• a key

The second argument is the key of the entry, you want to destroy.

10 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

• If the entry does not exist in the map it will not panic (and it will compile).

• If you use for the second argument a different type than the key type, the program will not compile.

Let’s take an example:

If you want to delete the entry with index two from the map employees you can use the following code :

delete(employees, "ABC4")

The entry with the key "ABC4" will be destroyed from memory if it exists.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

16 Length
You can retrieve the number of entries in the map with the built-in len :

fmt.Println(len(employees))
// 3
// There are three entries into the map

// remove entry with index 2


delete(employees, "V6555")

fmt.Println(len(employeeMap))
// 2
// There are two entries into the map

17 Iterate over a map


You can use the for loop with a range clause to iterate over all entries of a map :

for k, v := range employeeMap {


fmt.Printf("Key: %s - Value: %s\n", k, v)
}
// Key: V6555 - Value: {Walter Van Der Bolstenberg M Sales Manager}
// Key: V45657 - Value: {John Ollivero M CEO}
// Key: V45658 - Value: {Frane Elindo F CTO}

17.0.1 Do not rely on iteration order!


Note that this code snippet will return the elements, not in the insertion order.

This is because order is not assured. If we try to run a second time the same script, we might have the following result :

Key: V45657 - Value: {John Ollivero M CEO}


Key: V45658 - Value: {Frane Elindo F CTO}
Key: V6555 - Value: {Walter Van Der Bolstenberg M Sales Manager}

Please keep this in mind as it can be a source of errors.

17.0.2 A solution to the order problem


You can solve this ordering problem by using another variable to store the insertion order. If the order is important to you, you can use this
solution :

order := []string{}
order = append(order, employeeID)
employeeMap[employeeID] = employee

Here we create a slice order. This slice will store the keys in the insertion order into the map. So each time we add an entry to the map, we
add the key to the slice by calling order = append(order, employeeID) .

This way, we can get the entries in the order of insertion :

11 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

for _,k := range order {


fmt.Printf("Key: %s - Value: %s\n", k, employees[k])
}

We iterate over the slice order to get the keys, and then we retrieve the entry value by calling employees[k] , where k represents a key of
the map employees .

18 Two-dimensional maps (map of maps)


In our previous example, we wanted to store data with the structure : enployeeID => enployeeData

The key is the enployeeID and the value is a structtype employee . But imagine that we do not want to store a struct but another map
instead :

Two-dimensional map[fig:Two-dimensional-map]

In the figure 2 there are two maps. The second map is of type map[string]string . We store as keys “Name”, “Position” and “Genre” and
the values are the corresponding employee data. The first map is of type map[int]map[string]string . The type notation is a little bit
confusing, but when you look at it closely it makes sense :

The value of the map is another map

The second map is the inner map. It is the value of the first map. Each entry of this type has an integer key and for value a
map[string]string.

Two-dimensional maps are, in my opinion, too complicated. You might better use a map with a struct value.

12 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

19 Test Yourself
19.1 Questions
1. How to check if a key/element pair is in a map?

2. How are Go maps implemented internally?

3. Which types are forbidden for map keys?

4. Why is it forbidden to use some types for keys of a map?

5. When you iterate over a map, then the runtime will return the keys and the elements in the order you inserted them. True or False ?

6. How to remove a key/element pair from a map?

7. How to get the number of key/element pairs in a map?

8. How to iterate over a map?

9. If a map M does not contain the key K, what will return M[K]?

19.2 Answers
1. How to check if a key/element pair is in a map?

1. Let’s say that you want to check if there is a key/element pair with a key equal to 102 in a map rooms: room, ok =
rooms[102]; . When ok is true, the pair exists.
2. How are Go maps implemented internally?

1. Internally, Go maps are hash tables.


3. Which types are forbidden for map keys?

1. functions, slices, maps

2. Any array composed of the previous types

3. Any type composed of at least one of those types

4. Why is it forbidden to use some types for keys of a map?

1. Because the comparison operators == and =* are not fully defined for those types. Go needs to be able to compare
keys in its internal implementation. \end{enumerate} \item When you iterate over a map, then the runtime will return
the keys and the elements in the order you inserted them. True or False ? \begin{enumerate} \item False. A map is an
unordered collection. Go will \textbf{not} keep the memory of the insertion order. You will have to save it yourself
if you need it. \end{enumerate} \item How to remove a key/element pair from a map? \lstinline{delete(employees,
"ABC4")} \begin{enumerate} \item When the element is not found, nothing will happen
5. How to get the number of key/element pairs in a map?

len(myMap)

6. How to iterate over a mapTakeaways?

1. With a for loop : for k, v := range employees


7. If a map M does not contain the key K , what will return M[K] ?

1. It will return the zero value of the element type

2. If the element is an int it will return 0 for instance.

The paper and the digital edition of this book are available here. ×
I also filmed a video course to build a real world project with Go.

20 Key Takeways
• A map is an unordered collection of elements (values) of type V that are indexed by unique keys of type K

• Map types are denoted like this : map[K]V

• An element inside a map is called a map entry or a key-value pair.

13 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

• To initialize a map, you can use the following syntaxes :

m := make(map[string]uint8)

m := map[string]uint8{ "This is the key":42}

• The zero value of the map type is nil .

var m map[string]uint8
log.Println(m)

◦ Will output nil


• To insert an element into a map, you can use the following syntax : m2[ "myNewKey"] = "the value"

• Important : a map should be initialized before used

◦ The following program will cause a panic :


var m map[string]uint8
m["test"] = 122

panic: assignment to entry in nil map

• To retrieve an element in a map, you can use the following syntax

m := make(map[string]uint8)// fill the mapvalueRetrieved := m[ "myNewKey"]

◦ When no value is found, the variable valueRetrieved will be equal to the zero value of the map value type.

▪ Here valueRetrieved will be equal to 0 (zero value of type uint8 )


m := make(map[string]uint8)
// fill the map
valueRetrieved, ok := m[ "myNewKey"]
if ok {
// found an entry in the map with the key "myNewKey"

} else {
// not found :(

ok is a boolean which is equal to true if an entry with that key exists in the map

• You can iterate over a map with a for loop (with range clause)

◦ Warning: the order of insertion may not be used (it is not guaranteed)!

◦ To keep in memory the order of insertion in the map, create a slice and append each key to it

◦ Then you can iterate over the slice and fetch each value in the order of insertion.

• Insertion and lookup in a map are very quick, even if the map has many entries.

1. https://golang.org/ref/spec#Map_types↩

2. Go Specs https://golang.org/ref/spec#Map_types↩

3. https://en.wikipedia.org/wiki/Computational_complexity↩

4. https://en.wikipedia.org/wiki/Time_complexity↩

5. You can find more info about this notation on this Wikipedia article: https://en.wikipedia.org/wiki/Big_O_notation↩

Bibliography
• [stevens2006fast] Stevens, Marc. 2006. “Fast Collision Attack on Md5.” IACR Cryptology ePrint Archive 2006: 104.

Previous Next

Errors

14 of 15 02/01/2023, 02:13
Maps - Practical Go Lessons https://www.practical-go-lessons.com/chap-22-maps

Slices

Table of contents

Did you spot an error ? Want to give me feedback ? Here is the feedback page! ×

Newsletter:
Like what you read ? Subscribe to the newsletter.

I will keep you informed about the book updates.

@ [email protected]

Practical Go Lessons
By Maximilien Andile
Copyright (c) 2023
Follow me Contents
Posts
Book
Support the author Video Tutorial

About
The author
Legal Notice
Feedback
Buy paper or digital copy
Terms and Conditions

15 of 15 02/01/2023, 02:13

You might also like