diff --git a/doc.go b/doc.go new file mode 100644 index 0000000..3a807b4 --- /dev/null +++ b/doc.go @@ -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 diff --git a/syncmap.go b/syncmap.go index fb2c695..8f27cdd 100644 --- a/syncmap.go +++ b/syncmap.go @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 { @@ -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 {