tsmap

tsmap reads and writes maps shared across goroutines, taking a caller-supplied lock at every call site.

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) require threadsafe.Locker and use Lock/Unlock.
  • read and pure-transform operations (Get, GetOK, Len, Snapshot, Filter, Map, Reduce, Invert, RDo) require threadsafe.RLocker and use RLock/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.