tsslice

package
v1.151.0 Latest Latest
Warning

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

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

Documentation

Overview

Package tsslice solves the common concurrency problem of safely operating on Go slices shared across multiple goroutines without repeating lock boilerplate around every access.

Problem

Slices are lightweight and ubiquitous, but concurrent reads and writes to shared slice state can cause data races and undefined behavior. In practice, teams end up duplicating `Lock`/`Unlock` and `RLock`/`RUnlock` blocks around simple operations (set, get, append, transform), making code noisy and increasing the chance of forgetting synchronization on one code path.

tsslice provides lock-aware generic helpers that keep synchronization explicit and consistent while preserving the ergonomics of slice utility operations.

How It Works

Each function takes a pointer to the slice and a lock interface from github.com/tecnickcom/nurago/pkg/threadsafe:

The helpers delegate functional operations to github.com/tecnickcom/nurago/pkg/sliceutil while enforcing synchronization around the access.

Key Features

  • Generic API for any slice type via `S ~[]E`.
  • Clear separation of mutation vs read/transform lock requirements.
  • Bounds-checked, non-panicking accessors (GetOK, SetOK) alongside the idiomatic indexing forms (Get, Set).
  • Atomic compound operations via Do (read-modify-write) and RDo (multi-step reads) for sequences a single helper cannot express.
  • Snapshot returns a consistent copy under the read lock for safe read-out.
  • Thread-safe functional utilities: Filter, Map, and Reduce for expressive transformations on shared slice state.
  • Minimal adoption cost: compatible with standard sync.RWMutex and custom lock implementations that satisfy 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

Index

Examples

Constants

This section is empty.

Variables

This section is empty.

Functions

func Append

func Append[S ~[]E, E any](mux threadsafe.Locker, s *S, v ...E)

Append appends one or more values using an exclusive lock.

Append may reallocate the backing array and reassign the slice pointed to by s. Because every other helper also takes *S and dereferences it under the lock, concurrent access through the helpers remains safe: see the package Concurrency notes.

Example (Concurrent)
package main

import (
	"fmt"
	"sort"
	"sync"

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

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

	maxgor := 5
	s := make([]int, 0, maxgor)

	for i := range maxgor {
		wg.Add(1)

		go func(item int) {
			defer wg.Done()

			tsslice.Append(mux, &s, item)
		}(i)
	}

	wg.Wait()

	sort.Ints(s)
	fmt.Println(s)

}
Output:
[0 1 2 3 4]
Example (Multiple)
package main

import (
	"fmt"
	"sync"

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

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

	s := make([]string, 0, 2)
	tsslice.Append(mux, &s, "Hello", "World")

	fmt.Println(s)

}
Output:
[Hello World]
Example (Simple)
package main

import (
	"fmt"
	"sync"

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

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

	s := make([]string, 0, 2)
	tsslice.Append(mux, &s, "Hello")
	tsslice.Append(mux, &s, "World")

	fmt.Println(s)

}
Output:
[Hello World]
Example (Slice)
package main

import (
	"fmt"
	"sync"

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

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

	s := make([]string, 0, 2)
	tsslice.Append(mux, &s, []string{"Hello", "World"}...)

	fmt.Println(s)

}
Output:
[Hello World]

func Delete

func Delete[S ~[]E, E any](mux threadsafe.Locker, s *S, k int) bool

Delete removes the element at index k using an exclusive lock, preserving the order of the remaining elements. It reports whether k was within range; an out-of-range index is a no-op that returns false, so Delete never panics.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"a", "b", "c", "d"}

	tsslice.Delete(mux, &s, 1)
	fmt.Println(s)

	fmt.Println(tsslice.Delete(mux, &s, 9))

}
Output:
[a c d]
false

func Do

func Do[S ~[]E, E any](mux threadsafe.Locker, s *S, f func(*S))

Do runs f while holding the exclusive lock, giving it raw access to the slice pointer for atomic compound operations (read-modify-write, conditional growth). f must not call back into these helpers on the same lock, and must not retain the pointer 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/tsslice"
)

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

	s := []int{1, 2, 3}

	// Atomically append 4 only if it is not already the last element.
	tsslice.Do(mux, &s, func(sp *[]int) {
		if len(*sp) == 0 || (*sp)[len(*sp)-1] != 4 {
			*sp = append(*sp, 4)
		}
	})

	fmt.Println(s)

}
Output:
[1 2 3 4]

func Filter

func Filter[S ~[]E, E any](mux threadsafe.RLocker, s *S, f func(int, E) bool) S

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

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"Hello", "World", "Extra"}

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

	s2 := tsslice.Filter(mux, &s, filterFn)

	fmt.Println(s2)

}
Output:
[World]

func Get

func Get[S ~[]E, E any](mux threadsafe.RLocker, s *S, k int) E

Get returns value at index k under a read lock.

Get panics if k is out of range, mirroring built-in slice indexing; use GetOK for a bounds-checked, non-panicking alternative.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"Hello", "World"}
	fmt.Println(tsslice.Get(mux, &s, 0))
	fmt.Println(tsslice.Get(mux, &s, 1))

}
Output:
Hello
World

func GetOK

func GetOK[S ~[]E, E any](mux threadsafe.RLocker, s *S, k int) (E, bool)

GetOK returns the value at index k under a read lock and reports whether k was within range. It never panics: an out-of-range index returns the zero value and false.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"Hello", "World"}

	v, ok := tsslice.GetOK(mux, &s, 1)
	fmt.Println(v, ok)

	v, ok = tsslice.GetOK(mux, &s, 5)
	fmt.Println(v, ok)

}
Output:
World true
 false

func Len

func Len[S ~[]E, E any](mux threadsafe.RLocker, s *S) int

Len returns slice length under a read lock.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"Hello", "World"}
	fmt.Println(tsslice.Len(mux, &s))

}
Output:
2

func Map

func Map[S ~[]E, E any, U any](mux threadsafe.RLocker, s *S, f func(int, E) U) []U

Map transforms each element under a read lock and returns a new slice.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []string{"Hello", "World", "Extra"}

	mapFn := func(k int, v string) int { return k + len(v) }

	s2 := tsslice.Map(mux, &s, mapFn)

	fmt.Println(s2)

}
Output:
[5 6 7]

func RDo

func RDo[S ~[]E, E any](mux threadsafe.RLocker, s *S, f func(S))

RDo runs f while holding the read lock, giving it the current slice value for atomic multi-step reads. f must not mutate the slice, call back into these helpers on the same lock, or retain the slice 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/tsslice"
)

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

	s := []int{1, 2, 3, 4}

	sum := 0

	tsslice.RDo(mux, &s, func(sv []int) {
		for _, v := range sv {
			sum += v
		}
	})

	fmt.Println(sum)

}
Output:
10

func Reduce

func Reduce[S ~[]E, E any, U any](mux threadsafe.RLocker, s *S, init U, f func(int, E, U) U) U

Reduce folds slice elements under a read lock starting from init.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []int{2, 3, 5, 7, 11}

	init := 97
	reduceFn := func(k, v, r int) int { return k + v + r }

	r := tsslice.Reduce(mux, &s, init, reduceFn)

	fmt.Println(r)

}
Output:
135

func Set

func Set[S ~[]E, E any](mux threadsafe.Locker, s *S, k int, v E)

Set assigns value v at index k using an exclusive lock.

Set panics if k is out of range, mirroring built-in slice indexing; use SetOK for a bounds-checked, non-panicking alternative.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := make([]string, 2)
	tsslice.Set(mux, &s, 0, "Hello")
	tsslice.Set(mux, &s, 1, "World")

	fmt.Println(s)

}
Output:
[Hello World]

func SetOK

func SetOK[S ~[]E, E any](mux threadsafe.Locker, s *S, k int, v E) bool

SetOK assigns value v at index k using an exclusive lock and reports whether k was within range. It never panics: an out-of-range index is a no-op that returns false.

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := make([]string, 2)

	fmt.Println(tsslice.SetOK(mux, &s, 0, "Hello"))
	fmt.Println(tsslice.SetOK(mux, &s, 9, "World"))
	fmt.Println(s)

}
Output:
true
false
[Hello ]

func Snapshot

func Snapshot[S ~[]E, E any](mux threadsafe.RLocker, s *S) S

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

Example
package main

import (
	"fmt"
	"sync"

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

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

	s := []int{1, 2, 3}

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

	fmt.Println(snap)

}
Output:
[1 2 3]

Types

type Guarded

type Guarded[E any] struct {
	// contains filtered or unexported fields
}

Guarded is a slice 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 slice. 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 slice or pointer 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/tsslice"
)

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

func NewGuarded

func NewGuarded[E any](s []E) *Guarded[E]

NewGuarded returns a Guarded that takes ownership of s. Pass nil for an initially empty slice.

Ownership must be exclusive: after the call the caller must not read or write s — nor any other slice that aliases the same backing array, such as a spare-capacity re-slice — except through the returned Guarded, otherwise those accesses race with the Guarded's methods.

func (*Guarded[E]) Append

func (g *Guarded[E]) Append(v ...E)

Append appends one or more values under an exclusive lock.

func (*Guarded[E]) Delete

func (g *Guarded[E]) Delete(k int) bool

Delete removes the element at index k under an exclusive lock, preserving the order of the remaining elements. It reports whether k was within range; an out-of-range index is a no-op that returns false, so Delete never panics.

func (*Guarded[E]) Do

func (g *Guarded[E]) Do(f func(s *[]E))

Do runs f under an exclusive lock, giving it a pointer to the slice for atomic compound operations (read-modify-write, conditional growth, transforms to a different type). f must not call back into this Guarded, nor retain the pointer beyond its own return.

func (*Guarded[E]) Filter

func (g *Guarded[E]) Filter(f func(int, E) bool) []E

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

func (*Guarded[E]) Get

func (g *Guarded[E]) Get(k int) E

Get returns the value at index k under a read lock. It panics if k is out of range, mirroring built-in slice indexing; use Guarded.GetOK for a bounds-checked, non-panicking alternative.

func (*Guarded[E]) GetOK

func (g *Guarded[E]) GetOK(k int) (E, bool)

GetOK returns the value at index k under a read lock and reports whether k was within range. It never panics: an out-of-range index returns the zero value and false.

func (*Guarded[E]) Len

func (g *Guarded[E]) Len() int

Len returns the slice length under a read lock.

func (*Guarded[E]) RDo

func (g *Guarded[E]) RDo(f func(s []E))

RDo runs f under a read lock, giving it the current slice value for atomic multi-step reads (including Map/Reduce to a different type via github.com/tecnickcom/nurago/pkg/sliceutil). f must not mutate the slice, call back into this Guarded, or retain the slice beyond its own return.

Example
package main

import (
	"fmt"

	"github.com/tecnickcom/nurago/pkg/sliceutil"
	"github.com/tecnickcom/nurago/pkg/threadsafe/tsslice"
)

func main() {
	// Transforms to a different element type go through RDo, which holds the
	// read lock while delegating to sliceutil.
	g := tsslice.NewGuarded([]int{1, 2, 3, 4})

	var lengths []int

	g.RDo(func(s []int) {
		lengths = sliceutil.Map(s, func(_ int, v int) int { return v * v })
	})

	fmt.Println(lengths)

}
Output:
[1 4 9 16]

func (*Guarded[E]) Set

func (g *Guarded[E]) Set(k int, v E)

Set assigns value v at index k under an exclusive lock. It panics if k is out of range; use Guarded.SetOK for a bounds-checked, non-panicking alternative.

func (*Guarded[E]) SetOK

func (g *Guarded[E]) SetOK(k int, v E) bool

SetOK assigns value v at index k under an exclusive lock and reports whether k was within range. It never panics: an out-of-range index is a no-op that returns false.

func (*Guarded[E]) Snapshot

func (g *Guarded[E]) Snapshot() []E

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

Jump to

Keyboard shortcuts

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