tsslice

tsslice reads and writes slices shared across goroutines, taking a caller-supplied lock at every access.

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