tsmap

package
v1.147.1 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 4 Imported by: 0

Documentation

Overview

Package tsmap solves the common concurrency problem of safely operating on Go maps shared across multiple goroutines without repeatedly writing lock/unlock boilerplate at every call site.

Problem

Go maps are not safe for concurrent read/write access. Teams typically wrap each operation with a mutex, but this creates repetitive, error-prone code and makes it easy to forget a lock around one path. Generic map transformations such as filter, map, reduce, and invert are especially noisy when each operation also needs synchronization.

tsmap provides lock-aware generic helpers that execute map operations under the appropriate lock contract from github.com/tecnickcom/nurago/pkg/threadsafe.

How It Works

Every function receives both the map and a lock interface:

This design keeps synchronization policy explicit at the call site while providing a concise, reusable API.

Key Features

  • Generic API for any map type: functions are parameterized over `M ~map[K]V`, so they work with plain maps and named map types.
  • Clear read/write lock separation: mutation helpers use exclusive locks, query/transform helpers use read locks.
  • Functional map utilities with safety built in: Filter, Map, Reduce, and Invert delegate to github.com/tecnickcom/nurago/pkg/maputil while preserving thread safety via the provided lock.
  • Presence-checked reads via GetOK, and atomic compound operations via Do (write) and RDo (read) for sequences a single helper cannot express.
  • Snapshot returns a consistent copy under the read lock for safe read-out.
  • Minimal adoption cost: works directly with standard sync.RWMutex (or any compatible custom lock type) through 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

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Delete

func Delete[M ~map[K]V, K comparable, V any](mux threadsafe.Locker, m M, k K)

Delete removes key k from map using an exclusive lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.Mutex{}

	m := map[int]string{0: "Hello", 1: "World"}

	tsmap.Delete(mux, m, 0)

	fmt.Println(m)

}
Output:
map[1:World]

func Do

func Do[M ~map[K]V, K comparable, V any](mux threadsafe.Locker, m M, f func(M))

Do runs f while holding the exclusive lock, giving it raw access to the map for atomic compound operations (check-then-set, bulk mutation). The map is passed by value, so it must be non-nil for f to insert entries (a nil map panics on assignment, exactly as Set does). f must not call back into these helpers on the same lock, nor retain the map beyond its own return: using it after the lock is released races with other goroutines.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.Mutex{}

	m := map[string]int{"a": 1}

	// Atomically set "b" only if it is absent, then bump "a".
	tsmap.Do(mux, m, func(mm map[string]int) {
		if _, ok := mm["b"]; !ok {
			mm["b"] = 2
		}

		mm["a"]++
	})

	fmt.Println(m["a"], m["b"])

}
Output:
2 2

func Filter

func Filter[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, f func(K, V) bool) M

Filter returns a new map containing entries for which predicate f is true, under a read lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]string{0: "Hello", 1: "World"}

	filterFn := func(_ int, v string) bool { return v == "World" }

	s2 := tsmap.Filter(mux, m, filterFn)

	fmt.Println(s2)

}
Output:
map[1:World]

func Get

func Get[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, k K) V

Get returns value for key k under a read lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]string{0: "Hello", 1: "World"}
	fmt.Println(tsmap.Get(mux, m, 0))
	fmt.Println(tsmap.Get(mux, m, 1))

}
Output:
Hello
World

func GetOK

func GetOK[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, k K) (V, bool)

GetOK returns value and presence flag for key k under a read lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]string{0: "Hello", 1: "World"}

	v, ok := tsmap.GetOK(mux, m, 0)
	fmt.Println(v, ok)

	v, ok = tsmap.GetOK(mux, m, 3)
	fmt.Println(v, ok)

}
Output:
Hello true
 false

func Invert

func Invert[M ~map[K]V, K, V comparable](mux threadsafe.RLocker, m M) map[V]K

Invert returns a new map with keys and values swapped, under a read lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]int{1: 10, 2: 20}

	s2 := tsmap.Invert(mux, m)

	fmt.Println(s2)

}
Output:
map[10:1 20:2]

func Len

func Len[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M) int

Len returns map length under a read lock.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]string{0: "Hello", 1: "World"}
	fmt.Println(tsmap.Len(mux, m))

}
Output:
2

func Map

func Map[M ~map[K]V, K, J comparable, V, U any](mux threadsafe.RLocker, m M, f func(K, V) (J, U)) map[J]U

Map transforms each map entry under a read lock and returns a new mapped result.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]string{0: "Hello", 1: "World"}

	mapFn := func(k int, v string) (string, int) { return "_" + v, k + 1 }

	s2 := tsmap.Map(mux, m, mapFn)

	fmt.Println(s2)

}
Output:
map[_Hello:1 _World:2]

func RDo

func RDo[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M, f func(M))

RDo runs f while holding the read lock for atomic multi-step reads. f must not mutate the map, call back into these helpers on the same lock, or retain the map beyond its own return: reading it after the lock is released races with other goroutines.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[string]int{"a": 1, "b": 2, "c": 3}

	sum := 0

	tsmap.RDo(mux, m, func(mm map[string]int) {
		for _, v := range mm {
			sum += v
		}
	})

	fmt.Println(sum)

}
Output:
6

func Reduce

func Reduce[M ~map[K]V, K comparable, V, U any](mux threadsafe.RLocker, m M, init U, f func(K, V, U) U) U

Reduce folds map entries under a read lock starting from init.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[int]int{0: 2, 1: 3, 2: 5, 3: 7, 4: 11}
	init := 97
	reduceFn := func(k, v, r int) int { return k + v + r }

	r := tsmap.Reduce(mux, m, init, reduceFn)

	fmt.Println(r)

}
Output:
135

func Set

func Set[M ~map[K]V, K comparable, V any](mux threadsafe.Locker, m M, k K, v V)

Set stores value v at key k using an exclusive lock. Like a built-in map assignment, Set panics if m is nil; use Guarded, which allocates lazily, to avoid pre-initializing the map.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.Mutex{}

	m := make(map[int]string, 2)
	tsmap.Set(mux, m, 0, "Hello")
	tsmap.Set(mux, m, 1, "World")

	fmt.Println(m)

}
Output:
map[0:Hello 1:World]

func Snapshot

func Snapshot[M ~map[K]V, K comparable, V any](mux threadsafe.RLocker, m M) M

Snapshot returns a shallow copy of the map taken under a read lock. The copy is safe to use after the lock is released, but any reference-typed values remain shared with the original map. It is the free-function counterpart of Guarded.Snapshot.

Example
package main

import (
	"fmt"
	"sync"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	mux := &sync.RWMutex{}

	m := map[string]int{"a": 1, "b": 2}

	// Snapshot returns a copy that is safe to use after the lock is released.
	snap := tsmap.Snapshot(mux, m)

	fmt.Println(len(snap), snap["a"], snap["b"])

}
Output:
2 1 2

Types

type Guarded

type Guarded[K comparable, V any] struct {
	// contains filtered or unexported fields
}

Guarded is a map bundled with its own sync.RWMutex. All access goes through its methods, which take the appropriate lock, so the lock can never be mismatched with the data or forgotten.

The zero value is ready to use and holds an empty map; write methods allocate the backing map lazily on first use. A Guarded must not be copied after first use; pass it by pointer (see NewGuarded).

Method callbacks passed to Guarded.Filter, Guarded.Do, and Guarded.RDo run while the lock is held: they must be cheap, non-blocking, must not call back into the same Guarded (doing so deadlocks), and must not retain the map they receive beyond their return (using it after the lock is released races with other goroutines).

Example
package main

import (
	"fmt"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	// 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

func NewGuarded

func NewGuarded[K comparable, V any](m map[K]V) *Guarded[K, V]

NewGuarded returns a Guarded that takes ownership of m. Pass nil for an initially empty map.

Ownership must be exclusive: after the call the caller must not read or write m except through the returned Guarded, otherwise those accesses race with the Guarded's methods.

func (*Guarded[K, V]) Delete

func (g *Guarded[K, V]) Delete(k K)

Delete removes key k under an exclusive lock. Deleting an absent key is a no-op.

func (*Guarded[K, V]) Do

func (g *Guarded[K, V]) Do(f func(m map[K]V))

Do runs f under an exclusive lock, giving it the map for atomic compound operations (check-then-set, bulk mutation). The map passed to f is never nil. f must not call back into this Guarded, nor retain the map beyond its own return.

Example
package main

import (
	"fmt"

	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	// Do runs a compound operation atomically under the write lock.
	g := tsmap.NewGuarded(map[string]int{"a": 1})

	g.Do(func(m map[string]int) {
		if _, ok := m["b"]; !ok {
			m["b"] = 2
		}

		m["a"]++
	})

	fmt.Println(g.Get("a"), g.Get("b"))

}
Output:
2 2

func (*Guarded[K, V]) Filter

func (g *Guarded[K, V]) Filter(f func(K, V) bool) map[K]V

Filter returns a new map containing entries for which predicate f is true, under a read lock.

func (*Guarded[K, V]) Get

func (g *Guarded[K, V]) Get(k K) V

Get returns the value for key k under a read lock, or the zero value if the key is absent.

func (*Guarded[K, V]) GetOK

func (g *Guarded[K, V]) GetOK(k K) (V, bool)

GetOK returns the value and presence flag for key k under a read lock.

func (*Guarded[K, V]) Len

func (g *Guarded[K, V]) Len() int

Len returns the map length under a read lock.

func (*Guarded[K, V]) RDo

func (g *Guarded[K, V]) RDo(f func(m map[K]V))

RDo runs f under a read lock for atomic multi-step reads (including Map, Reduce, or Invert via github.com/tecnickcom/nurago/pkg/maputil). f must not mutate the map, call back into this Guarded, or retain the map beyond its own return.

Example
package main

import (
	"fmt"

	"github.com/tecnickcom/nurago/pkg/maputil"
	"github.com/tecnickcom/nurago/pkg/threadsafe/tsmap"
)

func main() {
	// Transforms such as Invert go through RDo, which holds the read lock while
	// delegating to maputil.
	g := tsmap.NewGuarded(map[int]int{1: 10, 2: 20})

	var inv map[int]int

	g.RDo(func(m map[int]int) {
		inv = maputil.Invert(m)
	})

	fmt.Println(inv[10], inv[20])

}
Output:
1 2

func (*Guarded[K, V]) Set

func (g *Guarded[K, V]) Set(k K, v V)

Set stores value v at key k under an exclusive lock.

func (*Guarded[K, V]) Snapshot

func (g *Guarded[K, V]) Snapshot() map[K]V

Snapshot returns a shallow copy of the map taken under a read lock. The copy is safe to read after the lock is released, but any reference-typed values remain shared with the guarded map.

Jump to

Keyboard shortcuts

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