threadsafe

threadsafe defines the Locker and RLocker interfaces, so concurrent containers can take a caller-supplied lock instead of hard-coding a mutex type.

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

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

Two interfaces and no implementation. Locker is Lock/Unlock; RLocker is RLock/RUnlock. That is the entire package.

Why an interface and not a mutex

A concurrent container normally embeds its lock:

type Cache struct {
    mu sync.RWMutex
    m  map[string]entry
}

The lock type is then fixed for every caller. None of them can supply an existing lock that already guards a wider critical section, substitute an instrumented lock that records contention, or drop to a plain sync.Mutex where the read/write split costs more than it saves.

Declaring the dependency as an interface moves all three to the call site:

type counter struct {
    mux    threadsafe.Locker
    values map[string]int
}

func (c *counter) inc(key string) {
    c.mux.Lock()
    defer c.mux.Unlock()

    c.values[key]++
}

sync.Mutex and sync.RWMutex both satisfy Locker already, and sync.RWMutex also satisfies RLocker, so nothing needs adapting. The split between the two interfaces is what lets a type declare that it only ever reads, and get concurrent readers for free when the caller passes an RWMutex.

Locker shares its method set with the standard library’s sync.Locker and is a defined type rather than an alias. RLocker has no standard-library equivalent, which is the actual gap: sync never declared the read half as an interface.

Where it is used

tsmap and tsslice are built on these two interfaces, taking the lock as their first argument at every call. That is what makes them helpers rather than containers, and it is why they compose with a lock that already guards something else.

Cost

Calling through an interface means a dynamic dispatch that a direct sync.Mutex call would not pay, and it prevents the inlining a concrete type would allow. For a lock acquisition this is noise next to the atomic operation and any contention. If profiling says otherwise for your workload, embed the concrete mutex instead: the interface is there to be useful, not to be mandatory.

Example

c := &counter{
    mux:    &sync.Mutex{},
    values: make(map[string]int),
}

var wg sync.WaitGroup

for range 100 {
    wg.Go(func() { c.inc("hits") })
}

wg.Wait()

fmt.Println(c.values["hits"])

// Output:
// 100

Full source is in example_threadsafe_test.go. More runnable examples are on pkg.go.dev.

Dependencies

This package reaches no external module: it uses only the Go standard library.