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.
Map type
Key-Value pair
Map entry
Hash table
Time complexity
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 :
\n\n// maps/without-maps/main.go\npackage main\n\nimport "fmt"\n\ntype testScore struct {\n studentName string\n score uint8\n}\n\nfunc main() {\n results := []testScore{\n {"John Doe", 20},\n {"Patrick Indexing", 15},\n //...\n //...\n {"Bob Ferring", 7},\n {"Claire Novalingua", 8},\n }\n fmt.Println(results)\n}
\nWe 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 :
\nfor _, result := range results {\n if result.studentName == "Claire Novalingua" {\n fmt.Println("Score Found:", result.score)\n }\n}
\nWhy is this solution not optimal?
\nWe 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.
A map is an unordered collection of elements of type T that are indexed by unique keys of type U1.
\n\nIn 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
\nAn element of a map is called a “map entry”. It’s also usually named a key-value pair.
\n\nmap[keyType]elementType
\nWith a map, you can do the following operations :
\nstore 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.
\nCan we put all types defined in Go for the key type? And for the value type?
\n\nYou 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
array of function, map, or slice
struct type that contains fields of type function, map, or slice
// FORBIDDEN: an array of slices\n[3][]int\n\n// FORBIDDEN : an array of functions\n[3]func(http.ResponseWriter, *http.Request)\n\n// FORBIDDEN: a type with a slice field\ntype Test struct {\n scores []int\n}\n//...
\n\nThe keys of a map must be distinct.
\nIf 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.
\n\nThe 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.
\nFor instance, an element can be a year, the score of a match, a type struct representing a user of an application...
\n\nYou can use the make builtin to allocate and initialize a new map :
\nm:=make(map[string]int)
\nm
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.
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:
\nworldCupWinners := map[int]string{\n 1930: "Uruguay",\n 1934: "Italy",\n 1938: "Italy",\n 1950: "Uruguay"}\nfmt.Println(worldCupWinners)\n//map[1930:Uruguay 1934:Italy 1938:Italy 1950:Uruguay]
\nIn 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.
\nNote also that after initializing a map, you can add new values to it. In our example, we can add another year to the map!
\nYou can also use the map literal syntax to create an empty map.
\na := map[int]string{}
\nIn the previous code listing, a is a map (initialized and allocated), but no key-value pairs are stored in it.
\n\nHere is a simplified view of how a hash table works. (the go implementation is slightly different) :
\nA hash table is composed of 3 elements :
\nA 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 :
\nFrom 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)
Store the value
into the container at index h
Retrieving a value from a given key will also make use of the hash function :
\nFrom the value
get the return value hash_function(key)
. It will return the container index.
Extract the data from the given container and return it to the user.
A good hash function must have the following qualities :
\nAvoid collisions of hashes :
\n1989
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
Compute an index to get the location of the data in a limited amount of time. (the hash function must be time-efficient)
The hash produced must be stable in time. The key should produce the same hash at each call.
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.
\nThe following time complexity applies in general for hash tables :
\nInsertion : 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!
\nWe say that it’s a constant-time algorithm. We also say that it’s order 1.I used here the Big-O notation5.
\n\nThis is an overview of how maps are implemented in Go. The internal implementation might change over time.
\nThe source code is located in the runtime package (runtime/map.go).
\nA 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).
To find an element into a map, the user will give a key.
\nThe 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.
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.
\nThe user provides the key and the element value
\nThe 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.
\nIn 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.
\n\nYou 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)
\nHere is an excerpt of the CSV file :
\nemployeeId,employeeName,genre,position\nV45657,John Ollivero,M,CEO\nV45658,Frane Elindo,F,CTO\nV6555,Walter Van Der Bolstenberg,M,Sales Manager
\n\nThe users will query an employee based on its unique Id.
\nWe 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
Elements : values of type employee
Let’s build the first part of the script (to read the data into the file)
\n// maps/reading-csv/main.go\npackage main\n\nimport (\n "encoding/csv"\n "fmt"\n "io"\n "log"\n "os"\n)\n\nfunc main() {\n file, err := os.Open("/Users/maximilienandile/Documents/DEV/goBook/maps/usages/employees.csv")\n if err != nil {\n log.Fatalf("impossible to open file %s", err)\n }\n\n defer file.Close()\n\n r := csv.NewReader(file)\n for {\n record, err := r.Read()\n if err == io.EOF {\n break\n }\n if err != nil {\n log.Fatal(err)\n }\n fmt.Println(record)\n }\n}
\nThe first step is to open the file employees.csv.
\nWe 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 :
\ntype employee struct {\n name string\n genre string\n position string\n}
\nThe preparatory work is done let’s jump to the map creation and usage
\n\n// initialize and allocate a new map\nemployees := make(map[string]employee)\n// ...\nemployee := employee{\n name: record[1],\n genre: record[2],\n position: record[3]}\n// Add a new entry to the map\nemployees[employeeId] = employee
\nTo add a pair composed of a key
and a element
simply use the following syntax : m[\\text{key}]=\\text{value}
To get an element from a map you have to know it’s key. They are two different ways to do it :
\n\nImagine that you are looking for the data related to employee number 3.
\nYou will retrieve the value (a struct employee) by calling :
\nwalter := employees["V6555"]
\nHere we assign to the variable walter the value contained into the map employeeMap with the key V6555.
\n\nBut what if the value does not exist? Will you make your program panic? Let’s take the risk :
\n// when there is no such pair\nghost := employees["ABC55555"]\nfmt.Println(ghost)\n//{ }\nfmt.Println(reflect.TypeOf(ghost))\n// main.employee
\nHere 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.
\n\nIn 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.
\nWe can guess that the employee does not exist, but it’s not 100% sure. Those empty fields can also come from a corrupted file.
\nThat’s why Go creators have provided a more clever way to retrieve an entry in a map.
\n\nThe alternative syntax is the following :
\nv, ok := myMap[k]
\nThe 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
\nthe key-value pair does not exist, v is populated with the null value of type valueType
\nOften you will see this idiom :
\n// lookup with two values assignment\nemployeeABC2, ok := employees["ABC2"]\nif ok {\n // the key-element pair exists in the map\n fmt.Println(employeeABC2)\n} else {\n fmt.Printf("No employee with ID 'ABC2'")\n}
\nIt’s possible to ignore the value if you just want to test the presence of a key into the map :
\n// ignore the value retrieved\n_, ok := employees["ABC3"]\nif ok {\n // the key-element pair exists in the map\n} else {\n fmt.Printf("No employee with ID 'ABC3'")\n}
\nIn the previous example, we are telling the compiler that we do not need the value retrieved by using the underscore (_) character in the assignation.
\nThere is a shorter way to make the same operation :
\n// shorter code\nif _, ok := employees["ABC4"]; ok {\n // the key-element pair exists in the map\n} else {\n fmt.Println("No employee with ID 'ABC4'")\n}
\nThe two values assignment and the ok value check are done in one line!
\n\nValues retrieved from a map are not addressable. You cannot print the memory address of a map value.
\nFor instance, the following code :
\nfmt.Printf("address of the100 %p", &employeeMap[100])
\nWill result in a compiler error :
\n./main.go:66:14: cannot take the address of employeeMap[100]
\nWhy 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 !
\n\nPlease 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.
\nThe garbage collector will not do its job and remove the unused memory.
\n\nYou 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)
\nIt takes:
\na map as first argument
a key
The second argument is the key of the entry, you want to destroy.
\nIf 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:
\nIf you want to delete the entry with index two from the map employees
you can use the following code :
delete(employees, "ABC4")
\nThe entry with the key \"ABC4\"
will be destroyed from memory if it exists.
You can retrieve the number of entries in the map with the built-in len :
\nfmt.Println(len(employees))\n// 3\n// There are three entries into the map\n\n// remove entry with index 2\ndelete(employees, "V6555")\n\nfmt.Println(len(employeeMap))\n// 2\n// There are two entries into the map
\n\nYou can use the for loop with a range clause to iterate over all entries of a map :
\nfor k, v := range employeeMap {\n fmt.Printf("Key: %s - Value: %s\\n", k, v)\n}\n// Key: V6555 - Value: {Walter Van Der Bolstenberg M Sales Manager}\n// Key: V45657 - Value: {John Ollivero M CEO}\n// Key: V45658 - Value: {Frane Elindo F CTO}
\n\nNote that this code snippet will return the elements, not in the insertion order.
\nThis is because order is not assured. If we try to run a second time the same script, we might have the following result :
\nKey: V45657 - Value: {John Ollivero M CEO}\nKey: V45658 - Value: {Frane Elindo F CTO}\nKey: V6555 - Value: {Walter Van Der Bolstenberg M Sales Manager}
\nPlease keep this in mind as it can be a source of errors.
\n\nYou 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 :
\norder := []string{}\norder = append(order, employeeID)\nemployeeMap[employeeID] = employee
\nHere 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 :
\nfor _,k := range order {\n fmt.Printf("Key: %s - Value: %s\\n", k, employees[k])\n}
\nWe 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
.
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 :
In the figure 2 there are two maps. The second map is of typemap[string]string
. We store as keys “Name”, “Position” and “Genre” and the values are the corresponding employee data. The first map is of typemap[int]map[string]string
. The type notation is a little bit confusing, but when you look at it closely it makes sense :
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.
\nTwo-dimensional maps are, in my opinion, too complicated. You might better use a map with a struct value.
\n\nHow to check if a key/element pair is in a map?
How are Go maps implemented internally?
Which types are forbidden for map keys?
Why is it forbidden to use some types for keys of a map?
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 ?
How to remove a key/element pair from a map?
How to get the number of key/element pairs in a map?
How to iterate over a map?
If a map M does not contain the key K, what will return M[K]?
How to check if a key/element pair is in a map?
\n102
in a map rooms: room, ok = rooms[102];
. When ok is true, the pair exists.How are Go maps implemented internally?
\nWhich types are forbidden for map keys?
\nfunctions, slices, maps
Any array composed of the previous types
Any type composed of at least one of those types
Why is it forbidden to use some types for keys of a map?
\n==
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
How to get the number of key/element pairs in a map?
\nlen(myMap)
How to iterate over a mapTakeaways?
\nfor k, v := range employees
If a map M
does not contain the key K
, what will return M[K]
?
It will return the zero value of the element type
If the element is an int it will return 0 for instance.
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.
To initialize a map, you can use the following syntaxes :
\nm := make(map[string]uint8)\n\nm := map[string]uint8{ "This is the key":42}
The zero value of the map type is nil
.
var m map[string]uint8\nlog.Println(m)
\nnil
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
\nvar m map[string]uint8\nm["test"] = 122\n\npanic: assignment to entry in nil map
To retrieve an element in a map, you can use the following syntax
\nm := 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.
valueRetrieved
will be equal to 0 (zero value of type uint8
)m := make(map[string]uint8)\n// fill the map\nvalueRetrieved, ok := m[ "myNewKey"]\nif ok {\n // found an entry in the map with the key "myNewKey"\n\n} else {\n // not found :(\n\n}
\nok
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)
\nWarning: 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.
https://golang.org/ref/spec#Map_types↩︎
Go Specs https://golang.org/ref/spec#Map_types↩︎
https://en.wikipedia.org/wiki/Computational_complexity↩︎
https://en.wikipedia.org/wiki/Time_complexity↩︎
You can find more info about this notation on this Wikipedia article: https://en.wikipedia.org/wiki/Big_O_notation↩︎
Previous
\n\t\t\t\t\t\t\t\t\tSlices
\n\t\t\t\t\t\t\t\tNext
\n\t\t\t\t\t\t\t\t\tErrors
\n\t\t\t\t\t\t\t\t