Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
78 changes: 78 additions & 0 deletions doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
// Copyright 2026 AxonOps Limited.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

// Package syncmap provides a type-safe, generic wrapper around
// [sync.Map].
//
// The standard [sync.Map] stores keys and values as any, which means
// every load and store requires a type assertion at the call site.
// SyncMap[K, V] moves those assertions inside the wrapper, giving
// callers compile-time type safety with no additional allocations and
// no runtime dependencies beyond the standard library.
//
// # Relationship to sync.Map
//
// SyncMap is a thin layer over sync.Map. It exposes the same set of
// operations — Load, Store, LoadOrStore, LoadAndDelete, Delete, and
// Range — with identical semantics and the same concurrency
// guarantees. Four convenience methods are added on top: Len, Map,
// Keys, and Items. The underlying sync.Map is not exported; use the
// typed methods exclusively.
//
// # When to use SyncMap
//
// sync.Map is optimised for two access patterns: (1) entries are
// written once and read many times, or (2) multiple goroutines each
// operate on disjoint sets of keys. For workloads that do not fit
// either pattern — for example, a cache that is frequently written by
// a single goroutine — a plain map protected by a sync.RWMutex will
// usually perform better.
//
// Use SyncMap (and sync.Map) when:
// - Many goroutines read the same keys concurrently.
// - The set of active keys is stable; writes are infrequent.
// - You want a lock-free path for the common read case.
//
// Use map + sync.RWMutex when:
// - The write rate is high or unpredictable.
// - You need snapshot-consistent reads of multiple keys at once.
// - The map is owned by a single goroutine.
//
// # Thread safety
//
// All methods on SyncMap are safe for concurrent use by multiple
// goroutines without additional locking. This guarantee is inherited
// directly from sync.Map.
//
// # Zero value
//
// The zero value of SyncMap is an empty map ready for use. It must
// not be copied after first use; the same restriction applies as for
// sync.Map and sync.Mutex.
//
// # Quick start
//
// var m syncmap.SyncMap[string, int]
//
// m.Store("hits", 1)
//
// if v, ok := m.Load("hits"); ok {
// fmt.Println(v) // 1
// }
//
// m.Range(func(k string, v int) bool {
// fmt.Printf("%s=%d\n", k, v)
// return true
// })
package syncmap
80 changes: 55 additions & 25 deletions syncmap.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,27 +12,21 @@
// See the License for the specific language governing permissions and
// limitations under the License.

// Package syncmap is a type-safe generic wrapper around sync.Map.
//
// It mirrors the sync.Map API with compile-time type safety via Go
// generics, so callers no longer pay the cost of type assertions at
// every call site. Zero runtime dependencies.
//
// A full godoc package overview lands in issue #6 (doc.go); this
// inline comment exists only to satisfy the revive.package-comments
// lint rule until then.
package syncmap

import "sync"

// SyncMap is a wrapper around sync.Map which uses generics to make accessing the map more convenient
// SyncMap is a type-safe, generic wrapper around [sync.Map].
//
// The zero value is an empty map ready for use. SyncMap must not be
// copied after first use.
type SyncMap[K comparable, V any] struct {
syncMap sync.Map
}

// Load returns the value stored in the map for a key, or nil if no
// value is present.
// The ok result indicates whether value was found in the map.
// Load returns the value stored in the map for key, or the zero value
// of V if no entry is present. The ok result reports whether an entry
// was found.
func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) {
v, ok := m.syncMap.Load(key)
if !ok {
Expand All @@ -42,21 +36,22 @@ func (m *SyncMap[K, V]) Load(key K) (value V, ok bool) {
return v.(V), ok
}

// Store sets the value for a key.
// Store sets the value associated with key.
func (m *SyncMap[K, V]) Store(key K, value V) {
m.syncMap.Store(key, value)
}

// LoadOrStore returns the existing value for the key if present.
// Otherwise, it stores and returns the given value.
// LoadOrStore returns the existing value for key if present.
// Otherwise it stores value and returns it.
// The loaded result is true if the value was loaded, false if stored.
func (m *SyncMap[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
a, l := m.syncMap.LoadOrStore(key, value)
return a.(V), l
}

// LoadAndDelete deletes the value for a key, returning the previous value if any.
// The loaded result reports whether the key was present.
// LoadAndDelete deletes the entry for key and returns its previous
// value, if any. The loaded result reports whether the key was
// present. If the key was not present, value is the zero value of V.
func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
v, l := m.syncMap.LoadAndDelete(key)
if !l {
Expand All @@ -66,20 +61,36 @@ func (m *SyncMap[K, V]) LoadAndDelete(key K) (value V, loaded bool) {
return v.(V), l
}

// Delete deletes the value for a key.
// Delete removes the entry for key. It is a no-op if the key is not
// present.
func (m *SyncMap[K, V]) Delete(key K) {
m.syncMap.Delete(key)
}

// Range calls f sequentially for each key and value present in the map.
// If f returns false, range stops the iteration.
// Range calls f sequentially for each key and value present in the
// map. If f returns false, Range stops iteration.
//
// Range does not correspond to a consistent snapshot of the map's
// contents: no key will be visited more than once, but if a value is
// stored or deleted concurrently (including by f), Range may reflect
// any mapping for that key during the iteration.
//
// Range may run in O(n) time even if f returns false after a constant
// number of calls, where n is the number of elements in the map at
// the start of the call.
func (m *SyncMap[K, V]) Range(f func(key K, value V) bool) {
m.syncMap.Range(func(key, value any) bool {
return f(key.(K), value.(V))
})
}

// Len returns the number of items in the map
// Len returns the number of entries in the map at the moment of the
// call. It runs in O(n) time by traversing the map with Range.
//
// Because the traversal is not atomic, concurrent stores and deletes
// may cause the returned count to differ from the number of entries
// visible to any single subsequent operation. Treat the result as an
// approximation, not a consistent snapshot.
func (m *SyncMap[K, V]) Len() int {
l := 0
m.syncMap.Range(func(key, value any) bool {
Expand All @@ -89,7 +100,13 @@ func (m *SyncMap[K, V]) Len() int {
return l
}

// Map returns the current contents of the map as a standard Go map
// Map returns a shallow copy of the map's contents as a plain Go map.
// It runs in O(n) time.
//
// The returned map is a point-in-time approximation: because the
// underlying Range traversal is not atomic, concurrent modifications
// may or may not be reflected in the result. The caller owns the
// returned map and may modify it freely.
func (m *SyncMap[K, V]) Map() map[K]V {
newMap := make(map[K]V)
m.Range(func(key K, value V) bool {
Expand All @@ -99,7 +116,13 @@ func (m *SyncMap[K, V]) Map() map[K]V {
return newMap
}

// Keys returns a slice containing the keys in the map
// Keys returns a slice of all keys present in the map at the moment
// of the call. It runs in O(n) time.
//
// The result is a point-in-time approximation. Concurrent stores and
// deletes may cause the slice to include keys that have since been
// removed, or to omit keys that were added during traversal. The
// order of keys is undefined.
func (m *SyncMap[K, V]) Keys() []K {
var keys []K
m.syncMap.Range(func(key, value any) bool {
Expand All @@ -109,7 +132,14 @@ func (m *SyncMap[K, V]) Keys() []K {
return keys
}

// Items returns a slice containing the items in the map
// Items returns a slice of all values present in the map at the
// moment of the call. It runs in O(n) time.
//
// The result is a point-in-time approximation. Concurrent stores and
// deletes may cause the slice to include values that have since been
// removed, or to omit values that were added during traversal. The
// order of values is undefined, and does not correspond to the order
// returned by Keys.
func (m *SyncMap[K, V]) Items() []V {
var items []V
m.syncMap.Range(func(key, value any) bool {
Expand Down