Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
Package tsmap reads and writes maps shared across goroutines, taking a caller-supplied lock at every call site.
How It Works
Every function receives both the map and a lock interface from github.com/tecnickcom/nurago/pkg/threadsafe:
- write operations (
Set,Delete,Do) requirethreadsafe.Lockerand useLock/Unlock. - read and pure-transform operations (
Get,GetOK,Len,Snapshot,Filter,Map,Reduce,Invert,RDo) requirethreadsafe.RLockerand useRLock/RUnlock.
Filter, Map, Reduce, and Invert delegate to
github.com/tecnickcom/nurago/pkg/maputil under the provided lock. The helpers
work with standard sync.RWMutex and any custom lock type that satisfies the
interfaces.
Usage
var (
mu sync.RWMutex
m = map`string`int{"a": 1, "b": 2}
)
tsmap.Set(&mu, m, "c", 3)
v, ok := tsmap.GetOK(&mu, m, "a")
_ = v
_ = ok
even := tsmap.Filter(&mu, m, func(_ string, n int) bool { return n%2 == 0 })
total := tsmap.Reduce(&mu, m, 0, func(_ string, n int, acc int) int { return acc + n })
_ = even
_ = total
Determinism
Go map iteration order is randomized, so the transform helpers inherit the
semantics of github.com/tecnickcom/nurago/pkg/maputil: Reduce is deterministic
only when its reducing function is order-independent (for example commutative
and associative), and Map and Invert follow last-write-wins when several
input entries map to the same output key.
Concurrency
Maps are reference types and no helper reassigns the caller’s map variable, so passing the map by value is safe: every helper serializes access to the shared map through the provided lock. Route all access through the helpers using the same lock instance; touching the map directly outside the lock is a data race.
Each helper acquires and releases the lock on its own, so a sequence of separate
helper calls is not atomic as a whole (a GetOK followed by a separate Set is
a classic check-then-act race). Use Do (write) or RDo (read) to run a
compound operation under a single lock acquisition:
tsmap.Do(&mu, m, func(mm map`string`int) {
if _, ok := mm["k"]; !ok {
mm["k"] = 1
}
})
The predicate/transform callbacks passed to Filter, Map, Reduce, Invert,
Do, and RDo run while the lock is held. They must be cheap and non-blocking,
and must not call back into these helpers on the same lock: a write helper
invoked from a read-locked callback deadlocks, recursive read locking is not safe
when a writer is waiting, and any helper invoked under Do’s exclusive lock
deadlocks outright. A callback must also not retain the map it receives beyond
its own return: once the lock is released, reading or writing it races with other
goroutines.
Filter, Map, and Invert return new maps, but any reference-typed values
they contain remain shared with the original: the helpers protect the map, not
the objects it points to.
Guarded Wrapper
The free functions above require the caller to pair the right lock with the
right map at every call site. Guarded is an optional higher-level type that
owns both a map and its sync.RWMutex, so access can only go through its methods
and a lock can never be mismatched or forgotten. Prefer it when a single map is
the unit of sharing; keep the free functions when one lock must guard several
containers at once. Transforms to a different type (Map/Reduce/Invert) are
expressed through Guarded.RDo.
See also: github.com/tecnickcom/nurago/pkg/threadsafe
When To Use
- A map is shared across goroutines and you want synchronization visible in the code, not hidden.
- You need generic helpers (keys, values, filter, get-or-set) that respect an external lock.
Example
// The guard owns both the map and its lock; every method is safe to call
// from multiple goroutines without pairing a separate mutex.
g := tsmap.NewGuarded(map[string]int{"a": 1})
g.Set("b", 2)
g.Delete("a")
fmt.Println(g.Len())
v, ok := g.GetOK("b")
fmt.Println(v, ok)
// Output:
// 1
// 2 true
Full source is in example_guarded_test.go. More runnable examples are on pkg.go.dev.
Dependencies
This package reaches no external module: it uses only the Go standard library.