Table of contents
Part of nurago, a collection of independent Go packages for backend services.
import "github.com/tecnickcom/nurago/pkg/threadsafe/tsslice"
Package tsslice reads and writes slices shared across goroutines, taking a caller-supplied lock at every access.
How It Works
Each function takes a pointer to the slice and a lock interface from github.com/tecnickcom/nurago/pkg/threadsafe:
- write operations (
Set,SetOK,Delete,Append,Do) requirethreadsafe.Lockerand use exclusiveLock/Unlock. - read and pure-transform operations (
Get,GetOK,Len,Snapshot,Filter,Map,Reduce,RDo) requirethreadsafe.RLockerand use sharedRLock/RUnlock.
The helpers delegate functional operations to github.com/tecnickcom/nurago/pkg/sliceutil
while enforcing synchronization around the access. Accessors come in panicking
indexing forms (Get, Set) and bounds-checked, non-panicking forms (GetOK,
SetOK). They work with standard sync.RWMutex and any custom lock type that
satisfies the interfaces.
Usage
var (
mu sync.RWMutex
s = []int{1, 2, 3}
)
tsslice.Set(&mu, &s, 0, 10)
v := tsslice.Get(&mu, &s, 1)
_ = v
tsslice.Append(&mu, &s, 4, 5)
even := tsslice.Filter(&mu, &s, func(_ int, n int) bool { return n%2 == 0 })
total := tsslice.Reduce(&mu, &s, 0, func(_ int, n int, acc int) int { return acc + n })
_ = even
_ = total
Concurrency
All helpers take the slice by pointer (*S) and dereference it only while
holding the lock. This includes Append, which may reallocate the backing
array and reassign the slice. As a result, concurrent calls that share the same
slice variable and the same lock are safe by default: a reader observes a
consistent slice header even while another goroutine grows the slice.
Pass the address of the shared variable to every helper and route all access through them using the same lock instance. Reading or writing the shared variable directly (outside a helper and outside the lock) is still 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. Use Do (write) or RDo (read) to run a
compound operation (for example a conditional append or a multi-step scan) under
a single lock acquisition:
var (
mu sync.RWMutex
s = []int{1, 2, 3}
)
tsslice.Do(&mu, &s, func(sp *[]int) {
if len(*sp) < 4 {
*sp = append(*sp, 4)
}
})
The predicate/transform callbacks passed to Filter, Map, Reduce, 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 slice it receives (or the pointer
Do passes) beyond its own return: once the lock is released, reading or writing
that reference races with other goroutines.
Filter and Map return new slices, but any reference-typed elements they
contain remain shared with the original: the helpers protect the slice, not the
objects it points to.
Guarded Wrapper
The free functions above require the caller to pair the right lock with the
right slice at every call site. Guarded is an optional higher-level type that
owns both a slice 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
slice is the unit of sharing; keep the free functions when one lock must guard
several containers at once. Transforms to a different element type (Map/Reduce)
are expressed through Guarded.RDo.
See also: github.com/tecnickcom/nurago/pkg/threadsafe
When To Use
- A slice is appended to and read from several goroutines.
- You want the lock to remain the caller’s, so multiple structures can share one.
Example
// The guard owns both the slice and its lock; every method is safe to call
// from multiple goroutines without pairing a separate mutex.
g := tsslice.NewGuarded([]int{1, 2, 3})
g.Append(4, 5)
g.Set(0, 10)
g.Delete(1) // removes value 2, preserving order
fmt.Println(g.Snapshot())
fmt.Println(g.Len())
v, ok := g.GetOK(0)
fmt.Println(v, ok)
// Output:
// [10 3 4 5]
// 4
// 10 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.