safemap API

safemap

package

API reference for the safemap package.

S
struct

Map

Map is a generic thread-safe map.

It uses a sync.RWMutex to guard access to the underlying map.

pkg/safemap/map.go:13-16
type Map struct

Example

m := safemap.New[string, int]()
m.Set("key", 42)

Fields

Name Type Description
data map[K]V
mu sync.RWMutex
F
function

New

New creates a new empty SafeMap.

Returns

*Map[K,
V]
pkg/safemap/map.go:19-23
func New[K comparable, V any]() *Map[K, V]

{
	return &Map[K, V]{
		data: make(map[K]V),
	}
}
F
function

TestMap_Basic

Parameters

pkg/safemap/map_test.go:8-29
func TestMap_Basic(t *testing.T)

{
	m := New[string, int]()

	m.Set("a", 1)
	m.Set("b", 2)

	if v, ok := m.Get("a"); !ok || v != 1 {
		t.Errorf("Get a: got %d, want 1", v)
	}

	if !m.Has("b") {
		t.Error("Has b: should be true")
	}

	if m.Has("c") {
		t.Error("Has c: should be false")
	}

	if m.Len() != 2 {
		t.Errorf("Len: got %d, want 2", m.Len())
	}
}
F
function

TestMap_Delete

Parameters

pkg/safemap/map_test.go:31-39
func TestMap_Delete(t *testing.T)

{
	m := New[string, string]()
	m.Set("key", "value")
	m.Delete("key")

	if m.Has("key") {
		t.Error("key should be deleted")
	}
}
F
function

TestMap_Clear

Parameters

pkg/safemap/map_test.go:41-50
func TestMap_Clear(t *testing.T)

{
	m := New[int, int]()
	m.Set(1, 100)
	m.Set(2, 200)
	m.Clear()

	if m.Len() != 0 {
		t.Errorf("Clear: Len should be 0, got %d", m.Len())
	}
}
F
function

TestMap_Keys

Parameters

pkg/safemap/map_test.go:52-61
func TestMap_Keys(t *testing.T)

{
	m := New[string, int]()
	m.Set("x", 1)
	m.Set("y", 2)

	keys := m.Keys()
	if len(keys) != 2 {
		t.Errorf("Keys: got %d, want 2", len(keys))
	}
}
F
function

TestMap_Values

Parameters

pkg/safemap/map_test.go:63-72
func TestMap_Values(t *testing.T)

{
	m := New[string, int]()
	m.Set("x", 10)
	m.Set("y", 20)

	vals := m.Values()
	if len(vals) != 2 {
		t.Errorf("Values: got %d, want 2", len(vals))
	}
}
F
function

TestMap_Range

Parameters

pkg/safemap/map_test.go:74-89
func TestMap_Range(t *testing.T)

{
	m := New[string, int]()
	m.Set("a", 1)
	m.Set("b", 2)
	m.Set("c", 3)

	count := 0
	m.Range(func(k string, v int) bool {
		count++
		return count < 2
	})

	if count != 2 {
		t.Errorf("Range should stop early, got %d iterations", count)
	}
}
F
function

TestMap_GetOrSet

Parameters

pkg/safemap/map_test.go:91-103
func TestMap_GetOrSet(t *testing.T)

{
	m := New[string, int]()

	v1 := m.GetOrSet("key", 42)
	if v1 != 42 {
		t.Errorf("GetOrSet first call: got %d, want 42", v1)
	}

	v2 := m.GetOrSet("key", 100)
	if v2 != 42 {
		t.Errorf("GetOrSet second call: got %d, want 42", v2)
	}
}
F
function

TestMap_Compute

Parameters

pkg/safemap/map_test.go:105-123
func TestMap_Compute(t *testing.T)

{
	m := New[string, int]()

	m.Compute("counter", func(v int, exists bool) int {
		return v + 1
	})

	if v, _ := m.Get("counter"); v != 1 {
		t.Errorf("Compute first: got %d, want 1", v)
	}

	m.Compute("counter", func(v int, exists bool) int {
		return v + 1
	})

	if v, _ := m.Get("counter"); v != 2 {
		t.Errorf("Compute second: got %d, want 2", v)
	}
}
F
function

TestMap_Concurrent

Parameters

pkg/safemap/map_test.go:125-144
func TestMap_Concurrent(t *testing.T)

{
	m := New[int, int]()
	var wg sync.WaitGroup

	for i := 0; i < 100; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			m.Set(n, n*10)
			m.Get(n)
			m.Has(n)
		}(i)
	}

	wg.Wait()

	if m.Len() != 100 {
		t.Errorf("Concurrent: got %d items, want 100", m.Len())
	}
}
T
type

Hasher

Hasher is a function that returns a hash for a key.

pkg/safemap/sharded.go:9-9
type Hasher func(K) uint64
S
struct

ShardedMap

ShardedMap is a thread-safe map partitioned into multiple shards to reduce lock contention.

pkg/safemap/sharded.go:12-16
type ShardedMap struct

Fields

Name Type Description
shards []*Map[K, V]
mask uint64
hasher Hasher[K]
F
function

NewSharded

NewSharded creates a new ShardedMap.

shardCount must be a power of 2. If not, it will be rounded up to the next power of 2.
Default shardCount is 32 if input <= 0.

Parameters

hasher
Hasher[K]
shardCount
int

Returns

*ShardedMap[K,
V]
pkg/safemap/sharded.go:22-41
func NewSharded[K comparable, V any](hasher Hasher[K], shardCount int) *ShardedMap[K, V]

{
	if shardCount <= 0 {
		shardCount = 32
	}
	// Ensure power of 2
	if bits.OnesCount(uint(shardCount)) != 1 {
		shardCount = 1 << bits.Len(uint(shardCount))
	}

	shards := make([]*Map[K, V], shardCount)
	for i := 0; i < shardCount; i++ {
		shards[i] = New[K, V]()
	}

	return &ShardedMap[K, V]{
		shards: shards,
		mask:   uint64(shardCount - 1),
		hasher: hasher,
	}
}
F
function

StringHasher

StringHasher returns a hasher for string keys.

Parameters

s
string

Returns

uint64
pkg/safemap/sharded.go:114-118
func StringHasher(s string) uint64

{
	h := fnv.New64a()
	h.Write([]byte(s))
	return h.Sum64()
}
F
function

TestShardedMap_Basic

Parameters

pkg/safemap/sharded_test.go:11-46
func TestShardedMap_Basic(t *testing.T)

{
	m := NewSharded[string, int](StringHasher, 4)

	m.Set("foo", 1)
	m.Set("bar", 2)

	if v, ok := m.Get("foo"); !ok || v != 1 {
		t.Errorf("Get(foo) = %v, %v; want 1, true", v, ok)
	}

	if v, ok := m.Get("bar"); !ok || v != 2 {
		t.Errorf("Get(bar) = %v, %v; want 2, true", v, ok)
	}

	if _, ok := m.Get("baz"); ok {
		t.Errorf("Get(baz) = _, true; want false")
	}

	if !m.Has("foo") {
		t.Errorf("Has(foo) = false; want true")
	}

	m.Delete("foo")
	if m.Has("foo") {
		t.Errorf("Has(foo) after delete = true; want false")
	}

	if m.Len() != 1 {
		t.Errorf("Len() = %d; want 1", m.Len())
	}

	m.Clear()
	if m.Len() != 0 {
		t.Errorf("Len() after Clear = %d; want 0", m.Len())
	}
}
F
function

TestShardedMap_Range

Parameters

pkg/safemap/sharded_test.go:48-64
func TestShardedMap_Range(t *testing.T)

{
	m := NewSharded[string, int](StringHasher, 4)
	count := 100
	for i := 0; i < count; i++ {
		m.Set(fmt.Sprintf("key-%d", i), i)
	}

	seen := 0
	m.Range(func(k string, v int) bool {
		seen++
		return true
	})

	if seen != count {
		t.Errorf("Range visited %d items; want %d", seen, count)
	}
}
F
function

TestShardedMap_Concurrent

Parameters

pkg/safemap/sharded_test.go:66-92
func TestShardedMap_Concurrent(t *testing.T)

{
	m := NewSharded[string, int](StringHasher, 16)
	var wg sync.WaitGroup
	workers := 10
	ops := 1000

	for i := 0; i < workers; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			for j := 0; j < ops; j++ {
				key := fmt.Sprintf("key-%d-%d", id, j)
				m.Set(key, j) // Write

				// Read same key or random key
				rKey := fmt.Sprintf("key-%d-%d", rand.Intn(workers), rand.Intn(ops))
				m.Get(rKey)

				if j%10 == 0 {
					m.Delete(key)
				}
			}
		}(i)
	}

	wg.Wait()
}
F
function

TestShardedMap_GetOrSet

Parameters

pkg/safemap/sharded_test.go:94-105
func TestShardedMap_GetOrSet(t *testing.T)

{
	m := NewSharded[string, int](StringHasher, 4)
	v := m.GetOrSet("foo", 10)
	if v != 10 {
		t.Errorf("GetOrSet(foo) = %d; want 10", v)
	}

	v = m.GetOrSet("foo", 20)
	if v != 10 {
		t.Errorf("GetOrSet(foo) existing = %d; want 10", v)
	}
}
F
function

TestShardedMap_Compute

Parameters

pkg/safemap/sharded_test.go:107-129
func TestShardedMap_Compute(t *testing.T)

{
	m := NewSharded[string, int](StringHasher, 4)
	m.Set("foo", 1)

	v := m.Compute("foo", func(existing int, exists bool) int {
		return existing + 1
	})

	if v != 2 {
		t.Errorf("Compute(foo) = %d; want 2", v)
	}

	v = m.Compute("bar", func(existing int, exists bool) int {
		if !exists {
			return 5
		}
		return existing
	})

	if v != 5 {
		t.Errorf("Compute(bar) = %d; want 5", v)
	}
}
F
function

TestShardedMap_KeysDistribution

Parameters

pkg/safemap/sharded_test.go:131-147
func TestShardedMap_KeysDistribution(t *testing.T)

{
	// Verify that keys are actually distributed across shards
	// This uses internal knowledge check
	m := NewSharded[string, int](StringHasher, 4)
	count := 1000
	for i := 0; i < count; i++ {
		m.Set(strconv.Itoa(i), i)
	}

	// Each shard should have roughly count/4 items
	// Not strict check due to potential collision, but shouldn't be 0
	for i, shard := range m.shards {
		if shard.Len() == 0 {
			t.Errorf("Shard %d is empty, bad distribution or small sample", i)
		}
	}
}