In Go a map is a hash map, represented as an array of buckets, each of which contains an array of key/value pairs
The hmap Struct
A map value is a pointer to a runtime.hmap structure:
A new hmap is created for everymap instance in the program
Every hmap will have a different hash0 random hash seed value. Therefore, the same key may have different hash values for different map instances
The number of buckets is always a power of 2, so hmap.B stores log2 of number of buckets to reduce the variable size
The maptype Struct
The runtime.maptype struct is an alias for abi.MapType:
Key, Elem and Bucket fields store the type information about key, value and bucket, respectively
A new maptype is only created for every unique (combination of key and value types) map declaration in the program
The maptype makes a mapgeneric for every combination of key and value types. By separating maptype from hmap every map with same key-value types combination reuses the same maptype instance
The bmap Struct
This struct describes a hash map bucket. A bucket is configured to store 8 key/value pairs
The tophash is an 8 element array containing the high order byte (HOB) of the hash value for each key in this bucket. This array is used to speed up key lookup: if two HOB values are different, we can skip full key comparison
There are a couple of predefined tophash values:
Following tophash is an array of bytes that stores the key/value pairs. The byte array packs all the keys and then all the values together for the respective bucket. Packing allows eliminating padding which would be needed to maintain proper alignment boundaries. For example, map[int64]int8 would have needed an extra 7 bit padding per key/value pair. By using packing the padding only has to be appended to the end of the byte array and not in between
The key and values are not directly represented in the bmap struct, because they can be of any type, the compiler can’t know the size of the bmap struct in advance. For example:
Overflow Buckets
When a bucket becomes full and a 9th key needs to be added, it would be inefficient to grow the map by doubling the number of buckets. Instead, Go creates an overflow bucket, linking it to the original one. The new key-value pair gets stored in this overflow bucket rather than forcing a full grow
Generic Behavior
The map runtime doesn’t use generics to enable a generic map implementation. Instead, map lookups, insertions, and deletions are implemented in the runtime package. During compilation, map operations are rewritten to calls to the runtime:
key is a pointer to the key
h is a pointer to the runtime.hmap structure
t is a pointer to the runtime.maptype structure
Eviction
When the buckets start getting 80% full (load factor is 6.5), the map will trigger a growth, which might double the number of buckets
A map starts with one bucket. The map grows when buckets are about 80% full (load factor is 6.5). When map grows it allocates 2x number of buckets
Growing starts with assigning a pointer called the “old bucket” pointer to the current bucket array. Then a new bucket array is allocated to hold twice the number of existing buckets. This could result in large allocations, but the memory is not initialized so the allocation is fast
Once the memory for the new bucket array is available, the key/value pairs from the old bucket array can be moved or “evacuated” to the new bucket array
Evacuations happen incrementally as key/value pairs are added or removed from the map. This allows to amortize the cost of copying elements between multiple map operations. During evacuation map operation take longer because they need to check both old and new arrays
The key/value pairs that are together in an old bucket could be moved to different buckets inside the new bucket array. The evacuation algorithm attempts to distribute the key/value pairs evenly across the new bucket array
Operations Implementation
Map Creation
Element Lookup
Element Insertion
Element Deletion
Memory Leaks
The number of buckets in a map cannot shrink. Therefore, removing elements from a map doesn’t impact the number of existing buckets; it just zeroes the slots in the buckets. A map can only grow and have more buckets; it never shrinks
There are a couple of solutions if we want to delete unused buckets:
Re-create a copy of the current map
Store pointers as values. It doesn’t solve the fact that we will have a significant number of buckets; however, each bucket entry will reserve only the size of a pointer for the value
Addressability
Map elements are not addressable, because if the map grows, the address is of the stale value
Thus, we can’t assign to struct field in the map:
Also, if the map is nil or does not contain an entry, m[0] would return the zero value. Therefore, attempting to assign to m[0].s would assign to a nonexistent zero value element
To overcome this problem, we need to copy the element and reassign:
However, we can assign to a slice element because it is a slice header containing a pointer to the underlying array:
Map Iteration Guarantees
The iteration order over maps is not specified and is not guaranteed to be the same from one iteration to the next
If a map entry that has not yet been reached is removed during iteration, the corresponding iteration value will not be produced
If a map entry is created during iteration, that entry may be produced during the iteration or may be skipped. The choice may vary for each entry created and from one iteration to the next