The Underlying Implementation and Concurrency Safety of Maps in Go
1. Basic Concepts and Features of Maps
The map in Go is an unordered key-value pair collection implemented as a hash table, offering O(1) time complexity for lookup, insertion, and deletion operations. Keys must be of a comparable type (comparable using the == operator), while values can be of any type.
2. Underlying Data Structure of Maps
The core structure of a map is runtime.hmap (defined in the Go source file runtime/map.go), which mainly contains the following key fields:
count: The current number of key-value pairs in the mapB: The logarithm of the number of buckets (actual number of buckets is 2^B)buckets: Pointer to the array of bucketsoldbuckets: Pointer to the old bucket array during expansionnoverflow: Number of overflow buckets
Each bucket (bucket) can store up to 8 key-value pairs. When a bucket is full, overflow buckets are linked via a linked list.
3. Operation Principles of Maps
3.1 Lookup Process
value := m[key]
Detailed steps:
- Calculate the hash value of the key
- Use the last B bits of the hash value to determine the bucket location
- Sequentially compare the
tophash(the high 8 bits of the hash value) and the key within the bucket - If a matching
tophashis found, compare the actual key values - If the match is successful, return the corresponding value
3.2 Insertion Process
m[key] = value
Steps:
- Locate the bucket position similar to the lookup process
- If the key already exists, update the corresponding value
- If the key does not exist, find an empty slot in the bucket to insert
- If the bucket is full, create a new overflow bucket and insert
3.3 Expansion Mechanism
Expansion is triggered when the following conditions are met:
- Load factor exceeds 6.5 (load factor = count / 2^B)
- Too many overflow buckets (noverflow >= 2^B when B < 15, or noverflow >= 2^15 when B >= 15)
Expansion uses a gradual strategy:
- Create a new bucket array (twice the size of the original)
- Gradually migrate data from old buckets to new buckets during each insertion or deletion operation
- Release old buckets after migration is complete
4. Concurrency Safety Issues with Maps
4.1 Problem Manifestation
Concurrent read and write operations on a map will cause a panic:
// Error example: Concurrent write operations
go func() { m[1] = 1 }()
go func() { m[2] = 2 }() // May panic: concurrent map writes
// Error example: Concurrent read and write operations
go func() { _ = m[1] }() // Read
go func() { m[2] = 2 }() // Write, may panic
4.2 Solutions
Solution 1: Using sync.Mutex
type SafeMap struct {
mu sync.RWMutex
m map[string]int
}
func (s *SafeMap) Get(key string) int {
s.mu.RLock()
defer s.mu.RUnlock()
return s.m[key]
}
func (s *SafeMap) Set(key string, value int) {
s.mu.Lock()
defer s.mu.Unlock()
s.m[key] = value
}
Solution 2: Using sync.Map (Suitable for read-heavy, write-light scenarios)
var m sync.Map
// Store key-value pair
m.Store("key", "value")
// Load value
if value, ok := m.Load("key"); ok {
fmt.Println(value)
}
// Atomic operation: Store if the key does not exist
m.LoadOrStore("key", "value")
5. Best Practices for Using Maps
- Pre-allocate Space During Initialization
// Pre-estimate the size to avoid frequent expansions
m := make(map[string]int, 100)
- Avoid Modifying a Map During Iteration
// Incorrect approach
for k, v := range m {
if condition(v) {
delete(m, k) // May lead to unexpected behavior
}
}
// Correct approach: Record keys to delete first
var keys []string
for k, v := range m {
if condition(v) {
keys = append(keys, k)
}
}
for _, k := range keys {
delete(m, k)
}
- Check If a Key Exists
if value, exists := m[key]; exists {
// Key exists
} else {
// Key does not exist
}
Understanding the underlying implementation of maps helps in writing more efficient Go code, especially when handling large-scale data or high-concurrency scenarios. Choosing an appropriate synchronization strategy can significantly improve program performance.