safemap

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Aug 13, 2025 License: MIT Imports: 7 Imported by: 0

README

safemap

Boilerplate reduction for concurrent maps

Two flavors of (mostly) thread-safe maps for Go, unified behind a single interface:

  • MutexMap – classic map with a sync.RWMutex. Fast, simple, boring (in a good way).
  • SyncMap – wrapper around sync.Map. Great for “write once, read a zillion times” or disjoint key sets.

Pick your fighter, keep your code the same.


Why this exists

Because sometimes you want:

  • The convenience of helpers like ChangeKey, ForEach, and slices/channels/iterators of keys/values/pairs.
  • A single interface so you can swap implementations without touching call sites.

Also because typing mu.RLock()/mu.RUnlock() for the 400th time is how keyboards die.


Quick Start

things := safemap.NewMutexMap[string, int]() // Or: safemap.NewSyncMap[string, int]()

things.Set("thing1", 123)

if things.Exists("thing1") {
    // woa
    if things.ChangeKey("thing1", "thing2") {
        // amazing!
    }
}

value, ok := things.Get("thing2")
if !ok {
    // bummer
}
fmt.Println("Got:", value)

// Slices
keys := things.KeysSlice()
fmt.Printf("keys len: %v", len(keys))

values := things.ValuessSlice()
fmt.Printf("keys len: %v", len(values))

all := things.AllSlice()
fmt.Printf("keys len: %v", len(all))

// Iterate (error-aware)
if err := things.ForEach(func(key string, value int) error {
    fmt.Printf("%s => %v\n", key, value)
    return nil
}); err != nil {
    fmt.Println("iteration problems:", err)
}

// Context-aware iteration (cancel early)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = things.ForEachContext(ctx, func(key string, value int) error {
    fmt.Printf("%s => %v\n", key, value)
    if value > 420 {
        cancel()
    }
    return nil
}); err != nil {
    fmt.Println("iteration problems:", err)
}

// Channels (closed when done)
for key := range things.KeysChan() {
    fmt.Println("key via chan:", key)
}
for value := range things.ValuesChan() {
    fmt.Println("value via chan:", value)
}
for kv := range things.AllChan() {
    fmt.Printf("pair via chan: %s => %v\n", kv.Key, kv.Value)
}

// Modern go iterators
for key, value := range things.All() {
    fmt.Println("iter pair:", key, value)
}
for key := range things.Keys() {
    fmt.Println("iter key:", key)
}
for value := range things.Values() {
    fmt.Println("iter value:", value)
}

if things.Delete("thing2") {
    // Goodbye!
}

API (short version)

type KeyValue[K comparable, V any] struct {
	Key   K
	Value V
}

type SafeMap[K comparable, V any] interface {
	// Classic
	Length() int
	Get(K) (V, bool)
	Set(K, V)
	ChangeKey(K, K) bool
	Delete(K) bool
	Exists(K) bool

	// Neat
	KeysSlice() []K
	ValuesSlice() []V
	AllSlice() []KeyValue[K, V]

	// Communicative
	KeysChan() <-chan K
	ValuesChan() <-chan V
	AllChan() <-chan KeyValue[K, V]

	// Spicy
	ForEach(func(K, V) error) error
	ForEachContext(context.Context, func(K, V) error) error

	// Magical
	All() iter.Seq2[K, V]
	Keys() iter.Seq[K]
	Values() iter.Seq[V]

	// Fatal
	Clear()
}
Highlights
  • ChangeKey: rename a key atomically (no overwrite semantics).
  • ForEach/ForEachContext: errors are collected with errors.Join; ctx lets you bail early.
  • Iterators (All/Keys/Values): clean for … range style.
  • AllSlice / AllChan: convenient pair snapshots or buffered streams.
  • Channels & slices: consistent, buffered channels; MutexMap fills under RLock then unlocks; SyncMap snapshots to avoid producer blocking if the consumer stops.

Choose your map

NewMutexMap[K, V]()

  • Backed by a standard map[K]V with a sync.RWMutex for reduced boilerplate.
  • Good general-purpose default.
  • Predictable performance, especially when reads/writes contend on the same keys.
  • Semantics: slice/channel/iterator helpers snapshot under a read lock and then operate without holding locks.

NewSyncMap[K, V]()

  • Backed by sync.Map.
  • Best when keys are mostly write-once/read-many or when many goroutines touch disjoint key sets.
  • Includes an internal atomic length counter so Length() is O(1).
  • Semantics: iterator helpers (All/Keys/Values) reflect a live view like sync.Map.Range; channel helpers take a snapshot first to avoid producer blocking if the consumer stops. This may mean large allocations depending on the size of the underlying map.

Notes & Gotchas

  • Locking behavior (MutexMap): reads use an RLock. Iterators/slices snapshot before iterating. Channels are buffered to the snapshot size so sends don’t block or hold the lock long. This is a shallow copy, and thus once the lock is released prior to iteration If your value type is a reference type or contains references (pointer, slice, map, channel, func, or a struct/array containing any of those), mutating that underlying data on a different thread may cause a data race during iteration. If your values are plain values (ints, bools, strings, structs of only value fields, etc.), mutation/reassignment during iteration from any thread is safe.
  • sync.Map semantics (SyncMap): excels at write-once/read-many and disjoint key workloads; not ideal for heavy hot-key churn. Iteration observes a live view and may miss/include entries based on timing (per Range contract).
  • Length(): exact for MutexMap. For SyncMap, Length() is best-effort and can drift under races. We increment on first successful Set of a new key and decrement on Delete. Concurrent Set/Delete on the same key (or overlapping operations) can momentarily over/under-count. If you need an exact count, do a Range and count.
Contribution

IF for some reason you think this is at all even modestly okayish software and want to add to it, freely open an Issue or PR.


License

See LICENSE. Short version: be cool.


FAQ (slightly sarcastic edition)

"Why not just use map + RWMutex?"
You can! This is that - plus a tidy interface and some ergonomic helpers you’d probably have ended up writing anyway.

"Is this a cache?”
No. It’s a map. You provide the policy, it provides reduced boilerplate and utilities.

"Compatibility promise?"
No. Though I will always bump the minor version and indicate breaking change(s) in the commit.

"Benchmarks?"
I'm not making any claims that this is stupid fast or anything. It's for developer friendliness more than it is for speed.

But I did abuse an LLM to write some benchmarks, they're probably okay. See benchmarks_test.go

Use the one that matches your workload, your numbers.


Happy mapping. Try not to fight over the keys.

Documentation

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type KeyValue added in v0.2.0

type KeyValue[K comparable, V any] struct {
	Key   K
	Value V
}

KeyValue is a simple pair for snapshotting or sorting.

type SafeMap

type SafeMap[K comparable, V any] interface {
	// Classic
	Length() int
	Get(K) (V, bool)
	Set(K, V)
	ChangeKey(K, K) bool
	Delete(K) bool
	Exists(K) bool

	// Neat
	KeysSlice() []K
	ValuesSlice() []V
	AllSlice() []KeyValue[K, V]

	// Communiucative
	KeysChan() <-chan K
	ValuesChan() <-chan V
	AllChan() <-chan KeyValue[K, V]

	// Spicy
	ForEach(func(K, V) error) error
	ForEachContext(context.Context, func(K, V) error) error

	// Magical
	All() iter.Seq2[K, V]
	Keys() iter.Seq[K]
	Values() iter.Seq[V]

	// Fatal
	Clear()
}

SafeMap defines a concurrency-safe map interface with common operations

func NewMutexMap

func NewMutexMap[K comparable, V any]() SafeMap[K, V]

NewMutexMap returns a generic SafeMap underpinned by a Go map with common methods wrapped in a sync.RWMutex

This map type is preferred for general use for concurrency-safe operations, but not for situations where there could be high read lock contention on an individual instance of this type of Map. For low write, high read contention variant of SafeMap, try NewSyncMap

func NewSyncMap

func NewSyncMap[K comparable, V any]() SafeMap[K, V]

NewSyncMap returns a generic SafeMap underpinned by a sync.Map.

The Map type is optimized for two common use cases: (1) when the entry for a given key is only ever written once but read many times, as in caches that only grow, or (2) when multiple goroutines read, write, and overwrite entries for disjointed sets of key

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL