Go Memory Model: Concurrent Data Access
This challenge focuses on understanding and implementing safe concurrent data access in Go. You will build a simple in-memory key-value store that can be accessed by multiple goroutines simultaneously without causing data corruption or race conditions. This is a fundamental concept for building robust Go applications.
Problem Description
Your task is to create a thread-safe in-memory key-value store. This store should support basic operations like setting a value for a given key, getting a value associated with a key, and deleting a key-value pair. The critical requirement is that these operations must be safe to call concurrently from multiple goroutines.
Key Requirements:
Set(key string, value interface{}): Adds or updates a key-value pair in the store.valuecan be of any type.Get(key string) (interface{}, bool): Retrieves the value associated with a key. Returns the value and a boolean indicating whether the key was found.Delete(key string): Removes a key-value pair from the store. If the key does not exist, this operation should do nothing.- Thread Safety: All operations (
Set,Get,Delete) must be safe to call concurrently from multiple goroutines without leading to data races.
Expected Behavior:
When multiple goroutines call Set, Get, and Delete on the same MemoryStore instance, the data within the store should remain consistent. For example, if one goroutine sets a value and another immediately tries to get it, the second goroutine should see the updated value, not a stale one. Similarly, deletions should be atomic.
Edge Cases:
- Accessing a non-existent key with
Get. - Deleting a non-existent key.
- Concurrent
Setoperations on the same key. - Concurrent
GetandSetoperations on the same key. - Concurrent
DeleteandGetoperations on the same key.
Examples
Example 1: Basic Operations
Input:
store := NewMemoryStore()
go store.Set("name", "Alice")
go store.Set("age", 30)
time.Sleep(100 * time.Millisecond) // Give goroutines time to execute
output1, found1 := store.Get("name")
output2, found2 := store.Get("age")
output3, found3 := store.Get("city")
Output:
output1: "Alice", found1: true
output2: 30, found2: true
output3: nil, found3: false
Explanation: The store successfully stored and retrieved string and integer values. The non-existent key "city" was correctly reported as not found.
Example 2: Concurrent Updates and Deletion
Input:
store := NewMemoryStore()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
key := fmt.Sprintf("key_%d", k)
store.Set(key, k)
}(i)
}
wg.Wait() // Ensure all sets are done
// Verify some values
for i := 0; i < 10; i++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
key := fmt.Sprintf("key_%d", k)
val, found := store.Get(key)
if !found || val.(int) != k {
// This should ideally not happen in a correct implementation
fmt.Printf("Error: Expected %d for %s, got %v, found %t\n", k, key, val, found)
}
}(i)
}
wg.Wait()
// Delete some keys concurrently
for i := 0; i < 50; i++ {
wg.Add(1)
go func(k int) {
defer wg.Done()
key := fmt.Sprintf("key_%d", k)
store.Delete(key)
}(i)
}
wg.Wait()
// Check a deleted key and a remaining key
val1, found1 := store.Get("key_5") // Should be deleted
val2, found2 := store.Get("key_75") // Should still exist
Output:
(Potentially some error messages if verification fails, but ideally none)
val1: nil, found1: false
val2: 75, found2: true
Explanation: Goroutines concurrently set 100 keys. Some keys were then concurrently deleted. The store correctly reflects that deleted keys are no longer present, while remaining keys retain their values.
Constraints
- The
MemoryStoremust be implemented using standard Go library features. No external dependencies are allowed. - The
MemoryStoreshould handle an arbitrary number of keys and values. - Performance should be reasonable for typical concurrent access patterns, but extreme micro-optimizations are not the primary focus. The correctness of concurrency is paramount.
Notes
- Consider how to protect the underlying data structure from concurrent modification.
- Think about which synchronization primitives in Go are most suitable for this task.
- The
interface{}type allows storing values of any Go type, making the store versatile. - A common approach involves using a map to store the key-value pairs, but the map itself is not inherently thread-safe.