tsslice

tsslice reads and writes slices shared across goroutines, either through functions taking a caller-supplied lock or through Guarded, which owns its own.

Part of nurago, a collection of independent Go packages for backend services.

import "github.com/tecnickcom/nurago/pkg/threadsafe/tsslice"

Concurrent access to a shared slice: read, write, append, delete and transform, each taking a lock so the access is serialised.

This package exposes two APIs where its sibling tsmap exposes one.

Two APIs, one of them safer

The function API mirrors tsmap: pass a threadsafe.Locker or RLocker at each call.

var (
    mu sync.RWMutex
    s  = []int{1, 2, 3}
)

tsslice.Append(&mu, &s, 4)
v, ok := tsslice.GetOK(&mu, &s, 0)

Guarded[E] bundles the slice with its own sync.RWMutex instead:

g := tsslice.NewGuarded([]int{1, 2, 3})

g.Append(4)
v, ok := g.GetOK(0)

Prefer Guarded unless you have a reason not to. The lock and the data cannot drift apart, a caller cannot forget the lock, and a caller cannot pass the wrong one. Its zero value is usable and holds an empty slice. The function API is for the case where the slice has to be guarded by a lock that already protects something else, so that a wider critical section stays consistent.

NewGuarded takes ownership. After the call the original slice must not be touched directly, and that includes any slice aliasing the same backing array, such as a spare-capacity re-slice. Those accesses race with the Guarded methods, and the race detector will not reliably catch them.

Why the functions take a pointer

tsslice.Set(&mu, &s, ...) takes *S where the map equivalent takes the map by value. Appending to a slice can outgrow the backing array and allocate a new one, which rewrites the slice header, so the caller’s variable has to be updated. A map is a reference type whose header never needs rewriting, so tsmap can take it by value.

Get panics, GetOK does not

Get and Set index under the lock and panic on an out-of-range index, matching built-in slice behaviour. GetOK and SetOK return a boolean instead.

The distinction matters more here than in single-threaded code. Between a Len call and an indexed Get, another goroutine can shrink the slice, so an index that was valid a moment ago may not be. Use GetOK, or do the whole thing inside RDo.

Compound operations

Each call takes and releases the lock on its own, so any two calls together are not atomic. Do hands the callback a *S under the write lock; RDo hands it the slice under the read lock.

The callback rules match tsmap, and the same one is dangerous: the callback must not retain the slice or pointer beyond its return. Once the lock drops, that reference races with every other goroutine, and nothing in the type system stops a closure from keeping it.

Snapshot copies under the read lock when you want something you own.

Transforms

Filter, Map and Reduce delegate to sliceutil under the lock, and inherit its behaviour, including Filter pre-sizing its result to the full input length.

Dependencies

This package reaches no external module: it uses only the Go standard library, plus threadsafe and sliceutil.