From e7c1a9a3d4426253a13dc90f609607c75560bb2b Mon Sep 17 00:00:00 2001 From: Johnny Miller <163300+millerjp@users.noreply.github.com> Date: Mon, 20 Apr 2026 19:10:06 +0200 Subject: [PATCH] docs: add doc.go and rewrite exported godoc (#11) Move the package-level documentation into a dedicated doc.go, covering relationship to sync.Map, when-to-use guidance, thread safety, zero-value usability, and a quick-start example. Remove the inline placeholder package comment that landed in #8 purely to satisfy revive.package-comments. Rewrite the godoc on every exported symbol (SyncMap, Load, Store, LoadOrStore, LoadAndDelete, Delete, Range, Len, Map, Keys, Items) to match or improve on sync.Map's stdlib prose. Len, Map, Keys and Items now explicitly call out their O(n) cost and point-in-time approximation under concurrent mutation; LoadAndDelete now states the typed-zero-value-on-miss contract. No behaviour change. No signature change. Coverage unchanged at 100%. --- doc.go | 78 ++++++++++++++++++++++++++++++++++++++++++++++++++++ syncmap.go | 80 +++++++++++++++++++++++++++++++++++++----------------- 2 files changed, 133 insertions(+), 25 deletions(-) create mode 100644 doc.go 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 {