go / syncs
I use these generic sync utilities to simplify concurrent Go code.
WaitGroup.Go (Go 1.25+)
Go 1.25 added sync.WaitGroup.Go which simplifies the common pattern:
// Before (Go 1.24 and earlier)
wg.Add(1)
go func() {
defer wg.Done()
process(t)
}()
// After (Go 1.25+)
wg.Go(func() { process(t) })
For timeout or cancellation scenarios, use WaitGroupChan below.
Map
A generic thread-safe map using sync.RWMutex:
type Map[K comparable, V any] struct {
mu sync.RWMutex
m map[K]V
}
func (m *Map[K, V]) Load(key K) (V, bool) {
m.mu.RLock()
defer m.mu.RUnlock()
v, ok := m.m[key]
return v, ok
}
func (m *Map[K, V]) Store(key K, value V) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[K]V)
}
m.m[key] = value
}
func (m *Map[K, V]) Delete(key K) {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.m, key)
}
func (m *Map[K, V]) LoadOrStore(key K, value V) (actual V, loaded bool) {
m.mu.Lock()
defer m.mu.Unlock()
if m.m == nil {
m.m = make(map[K]V)
}
if v, ok := m.m[key]; ok {
return v, true
}
m.m[key] = value
return value, false
}
Prefer this over sync.Map when entries change frequently
and you want type safety without assertions.
Semaphore
A counting semaphore using a buffered channel:
type Semaphore struct {
c chan struct{}
}
func NewSemaphore(n int) Semaphore {
return Semaphore{c: make(chan struct{}, n)}
}
func (s Semaphore) Acquire() {
s.c <- struct{}{}
}
func (s Semaphore) TryAcquire() bool {
select {
case s.c <- struct{}{}:
return true
default:
return false
}
}
func (s Semaphore) Release() {
<-s.c
}
Useful for limiting concurrent operations:
var sem = NewSemaphore(10) // max 10 concurrent
func process(item Item) error {
sem.Acquire()
defer sem.Release()
return doWork(item)
}
WaitGroupChan
A WaitGroup that exposes a done channel for select:
type WaitGroupChan struct {
n int64
done chan struct{}
}
func NewWaitGroupChan() *WaitGroupChan {
return &WaitGroupChan{done: make(chan struct{})}
}
func (wg *WaitGroupChan) Add(delta int) {
n := atomic.AddInt64(&wg.n, int64(delta))
if n == 0 {
close(wg.done)
}
}
func (wg *WaitGroupChan) Done() { wg.Add(-1) }
func (wg *WaitGroupChan) Wait() { <-wg.done }
func (wg *WaitGroupChan) DoneChan() <-chan struct{} { return wg.done }
Useful when you need to wait with a timeout or cancellation:
wg := NewWaitGroupChan()
wg.Add(len(tasks))
for _, t := range tasks {
go func(t Task) {
defer wg.Done()
process(t)
}(t)
}
select {
case <-wg.DoneChan():
fmt.Println("all done")
case <-ctx.Done():
fmt.Println("canceled")
case <-time.After(10 * time.Second):
fmt.Println("timeout")
}
AtomicValue
A generic wrapper around atomic.Value:
type AtomicValue[T any] struct {
v atomic.Value
}
type wrapped[T any] struct{ v T }
func (a *AtomicValue[T]) Load() T {
if x := a.v.Load(); x != nil {
return x.(wrapped[T]).v
}
var zero T
return zero
}
func (a *AtomicValue[T]) Store(v T) {
a.v.Store(wrapped[T]{v})
}
The wrapper type avoids atomic.Value's panic on storing
different concrete types for interface values.
var config AtomicValue[*Config]
// Writer
config.Store(loadConfig())
// Readers (lock-free)
cfg := config.Load()