From 0a221424e89810382a4fdc3844cd8bdfcc90e645 Mon Sep 17 00:00:00 2001 From: Yash Mehrotra Date: Tue, 21 Oct 2025 18:41:52 +0530 Subject: [PATCH] feat: add syncmap --- collections/syncmap/syncmap.go | 104 +++++++++++ collections/syncmap/syncmap_test.go | 278 ++++++++++++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 collections/syncmap/syncmap.go create mode 100644 collections/syncmap/syncmap_test.go diff --git a/collections/syncmap/syncmap.go b/collections/syncmap/syncmap.go new file mode 100644 index 0000000..bae7a19 --- /dev/null +++ b/collections/syncmap/syncmap.go @@ -0,0 +1,104 @@ +package syncmap + +import "sync" + +// SyncMap is like a Go sync.Map but type-safe using generics. +// +// The zero SyncMap is empty and ready for use. A SyncMap must not be copied after first use. +type SyncMap[K comparable, V any] struct { + m sync.Map + zeroV V +} + +func New[K comparable, V any]() SyncMap[K, V] { + return SyncMap[K, V]{} +} + +func (s *SyncMap[K, V]) Clear() { + s.m.Clear() +} + +func (s *SyncMap[K, V]) Load(key K) (V, bool) { + v, ok := s.m.Load(key) + if !ok { + return s.zeroV, false + } + return v.(V), true + +} + +// Store sets the value for a key. +func (s *SyncMap[K, V]) Store(key K, value V) { + s.m.Store(key, value) +} + +// LoadOrStore returns the existing value for the key if present. +// Otherwise, it stores and returns the given value. +// The loaded result is true if the value was loaded, false if stored. +func (s *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) { + v, loaded := s.m.LoadOrStore(key, value) + if !loaded { + return value, false + } + return v.(V), true +} + +// Delete deletes the value for a key. +func (s *SyncMap[K, V]) Delete(key K) { + s.m.Delete(key) +} + +// LoadAndDelete deletes the value for a key, returning the previous value if any. +// The loaded result reports whether the key was present. +func (s *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) { + v, loaded := s.m.LoadAndDelete(key) + if !loaded { + var zero V + return zero, false + } + return v.(V), true +} + +// CompareAndDelete deletes the entry for key if its value is equal to old. +// The old value must be of a comparable type. +// +// If there is no current value for key in the map, CompareAndDelete returns false +// (even if the old value is the nil interface value). +func (s *SyncMap[K, V]) CompareAndDelete(key K, old V) (deleted bool) { + return s.m.CompareAndDelete(key, old) +} + +// Swap swaps the value for a key and returns the previous value if any. +// The loaded result reports whether the key was present. +func (s *SyncMap[K, V]) Swap(key K, value V) (previous V, loaded bool) { + v, loaded := s.m.Swap(key, value) + if !loaded { + var zero V + return zero, false + } + return v.(V), true +} + +// CompareAndSwap swaps the old and new values for key +// if the value stored in the map is equal to old. +// The old value must be of a comparable type. +func (s *SyncMap[K, V]) CompareAndSwap(key K, old, new V) bool { + return s.m.CompareAndSwap(key, old, new) +} + +// Range calls f sequentially for each key and value present in the map. +// If f returns false, range stops the iteration. +// +// Range does not necessarily correspond to any consistent snapshot of the Map's +// contents: no key will be visited more than once, but if the value for any key +// is stored or deleted concurrently (including by f), Range may reflect any +// mapping for that key from any point during the Range call. Range does not +// block other methods on the receiver; even f itself may call any method on m. +// +// Range may be O(N) with the number of elements in the map even if f returns +// false after a constant number of calls. +func (s *SyncMap[K, V]) Range(f func(key K, value V) bool) { + s.m.Range(func(key, value any) bool { + return f(key.(K), value.(V)) + }) +} diff --git a/collections/syncmap/syncmap_test.go b/collections/syncmap/syncmap_test.go new file mode 100644 index 0000000..cf27700 --- /dev/null +++ b/collections/syncmap/syncmap_test.go @@ -0,0 +1,278 @@ +package syncmap + +import ( + "sync" + "testing" +) + +func TestSyncMap_LoadAndStore(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + val, ok := m.Load("key1") + if !ok { + t.Error("Expected key1 to be present") + } + if val != 100 { + t.Errorf("Expected 100, got %d", val) + } + + _, ok = m.Load("nonexistent") + if ok { + t.Error("Expected nonexistent key to return false") + } +} + +func TestSyncMap_LoadOrStore(t *testing.T) { + var m SyncMap[string, int] + + actual, loaded := m.LoadOrStore("key1", 100) + if loaded { + t.Error("Expected loaded to be false for new key") + } + if actual != 100 { + t.Errorf("Expected 100, got %d", actual) + } + + actual, loaded = m.LoadOrStore("key1", 200) + if !loaded { + t.Error("Expected loaded to be true for existing key") + } + if actual != 100 { + t.Errorf("Expected 100 (original value), got %d", actual) + } +} + +func TestSyncMap_Delete(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + m.Delete("key1") + + _, ok := m.Load("key1") + if ok { + t.Error("Expected key1 to be deleted") + } + + m.Delete("nonexistent") +} + +func TestSyncMap_LoadAndDelete(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + + val, loaded := m.LoadAndDelete("key1") + if !loaded { + t.Error("Expected loaded to be true") + } + if val != 100 { + t.Errorf("Expected 100, got %d", val) + } + + _, ok := m.Load("key1") + if ok { + t.Error("Expected key1 to be deleted") + } + + _, loaded = m.LoadAndDelete("nonexistent") + if loaded { + t.Error("Expected loaded to be false for non-existent key") + } +} + +func TestSyncMap_CompareAndDelete(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + + deleted := m.CompareAndDelete("key1", 200) + if deleted { + t.Error("Expected delete to fail with wrong value") + } + + val, ok := m.Load("key1") + if !ok || val != 100 { + t.Error("Expected key1 to still exist with value 100") + } + + deleted = m.CompareAndDelete("key1", 100) + if !deleted { + t.Error("Expected delete to succeed with correct value") + } + + _, ok = m.Load("key1") + if ok { + t.Error("Expected key1 to be deleted") + } + + deleted = m.CompareAndDelete("nonexistent", 100) + if deleted { + t.Error("Expected delete to fail for non-existent key") + } +} + +func TestSyncMap_Swap(t *testing.T) { + var m SyncMap[string, int] + + prev, loaded := m.Swap("key1", 100) + if loaded { + t.Error("Expected loaded to be false for non-existent key") + } + if prev != 0 { + t.Errorf("Expected zero value, got %d", prev) + } + + val, ok := m.Load("key1") + if !ok || val != 100 { + t.Error("Expected key1 to have value 100") + } + + prev, loaded = m.Swap("key1", 200) + if !loaded { + t.Error("Expected loaded to be true for existing key") + } + if prev != 100 { + t.Errorf("Expected 100, got %d", prev) + } + + val, ok = m.Load("key1") + if !ok || val != 200 { + t.Error("Expected key1 to have value 200") + } +} + +func TestSyncMap_CompareAndSwap(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + + swapped := m.CompareAndSwap("key1", 200, 300) + if swapped { + t.Error("Expected swap to fail with wrong old value") + } + + val, ok := m.Load("key1") + if !ok || val != 100 { + t.Error("Expected key1 to still have value 100") + } + + swapped = m.CompareAndSwap("key1", 100, 200) + if !swapped { + t.Error("Expected swap to succeed with correct old value") + } + + val, ok = m.Load("key1") + if !ok || val != 200 { + t.Error("Expected key1 to have value 200") + } + + swapped = m.CompareAndSwap("nonexistent", 100, 200) + if swapped { + t.Error("Expected swap to fail for non-existent key") + } +} + +func TestSyncMap_Range(t *testing.T) { + var m SyncMap[string, int] + + expected := map[string]int{ + "key1": 100, + "key2": 200, + "key3": 300, + } + + for k, v := range expected { + m.Store(k, v) + } + + found := make(map[string]int) + m.Range(func(key string, value int) bool { + found[key] = value + return true + }) + + if len(found) != len(expected) { + t.Errorf("Expected %d items, got %d", len(expected), len(found)) + } + + for k, v := range expected { + if found[k] != v { + t.Errorf("Expected %s=%d, got %d", k, v, found[k]) + } + } + + count := 0 + m.Range(func(key string, value int) bool { + count++ + return false // Stop after first iteration + }) + + if count != 1 { + t.Errorf("Expected Range to stop after 1 iteration, got %d", count) + } +} + +func TestSyncMap_Clear(t *testing.T) { + var m SyncMap[string, int] + + m.Store("key1", 100) + m.Store("key2", 200) + m.Store("key3", 300) + + m.Clear() + + count := 0 + m.Range(func(key string, value int) bool { + count++ + return true + }) + + if count != 0 { + t.Errorf("Expected 0 items after Clear, got %d", count) + } + + _, ok := m.Load("key1") + if ok { + t.Error("Expected key1 to be deleted after Clear") + } +} + +func TestSyncMap_Concurrent(t *testing.T) { + var m SyncMap[int, int] + var wg sync.WaitGroup + + for i := 0; i < 100; i++ { + wg.Add(1) + go func(val int) { + defer wg.Done() + m.Store(val, val*10) + }(i) + } + + wg.Wait() + + for i := 0; i < 100; i++ { + val, ok := m.Load(i) + if !ok { + t.Errorf("Expected key %d to exist", i) + } + if val != i*10 { + t.Errorf("Expected %d, got %d", i*10, val) + } + } + + for i := 0; i < 100; i++ { + wg.Add(2) + go func(val int) { + defer wg.Done() + m.Store(val, val*20) + }(i) + go func(val int) { + defer wg.Done() + m.Load(val) + }(i) + } + + wg.Wait() +}