The Underlying Implementation and Concurrency Safety of Maps in Go

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 map
  • B: The logarithm of the number of buckets (actual number of buckets is 2^B)
  • buckets: Pointer to the array of buckets
  • oldbuckets: Pointer to the old bucket array during expansion
  • noverflow: 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:

  1. Calculate the hash value of the key
  2. Use the last B bits of the hash value to determine the bucket location
  3. Sequentially compare the tophash (the high 8 bits of the hash value) and the key within the bucket
  4. If a matching tophash is found, compare the actual key values
  5. If the match is successful, return the corresponding value

3.2 Insertion Process

m[key] = value

Steps:

  1. Locate the bucket position similar to the lookup process
  2. If the key already exists, update the corresponding value
  3. If the key does not exist, find an empty slot in the bucket to insert
  4. 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

  1. Pre-allocate Space During Initialization
// Pre-estimate the size to avoid frequent expansions
m := make(map[string]int, 100)
  1. 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)
}
  1. 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.