sync

package module
v1.33.0 Latest Latest
Warning

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

Go to latest
Published: Jul 15, 2026 License: MIT Imports: 9 Imported by: 12

README ΒΆ

Gopher CircleCI codecov Go Report Card Go Reference

πŸ”„ go-sync

A small Go library (package sync) with focused concurrency helpers:

  • Convenience aliases for common sync primitives and typed atomics
  • Hook-driven execution (Wait, Timeout, Worker)
  • Group helpers (ErrorGroup, ErrorsGroup, SingleFlightGroup)
  • Typed wrappers for sync.Pool, sync.Map, and atomic.Value
  • A bytes.Buffer pool specialized for copy-and-reuse workflows

πŸ“¦ Install

Requires Go 1.26 or newer.

go get github.com/alexfalkowski/go-sync

πŸ§ͺ Local validation

Local Make targets use shared tooling from the bin submodule. After cloning the repository for development, initialize it before running Make targets:

git submodule sync && git submodule update --init
make dep

Then run the narrow target you need, such as make specs.

Benchmarks
make benchmarks
make benchtime=1s benchmarks
make benchmark
Fuzzes
make fuzzes
make map-fuzz
make value-fuzz
make pool-fuzz
make group-fuzz
make worker-fuzz
make package=. name=FuzzMapStringIntOperations fuzztime=1000x fuzz

🧭 Package layout

The public API is intentionally small:

  • Aliases: Once, Mutex, RWMutex, WaitGroup, Int32, Int64, Uint32, Uint64, Uintptr, Bool, Pointer[T]
  • Hooks and timeout helpers: Hook, Handler, ErrorHandler, ErrNoOnRunProvided, ErrTimeout, Wait, Timeout, IsTimeoutError
  • Worker: ErrWorkerFull, NewWorker, Worker.Schedule, Worker.TrySchedule, Worker.Wait
  • Future: Async, Future[T], Future.Await
  • Groups: ErrorGroup, ErrorsGroup, NewSingleFlightGroup, SingleFlightGroup, AnySingleFlightGroup, SingleFlightResult, AnySingleFlightResult
  • Pools and wrappers: AnyPool, NewPool, Pool[T], NewBufferPool, BufferPool, NewValue, Value[T], AnyValue, NewMap, Map[K, V], AnyMap

Most wrappers preserve the semantics of the standard library type they wrap while making those semantics easier to use from generic code.

πŸ” Aliases

The package re-exports a few commonly used concurrency primitives and helper types for convenience:

  • Once, Mutex, RWMutex, and WaitGroup alias their counterparts in sync.
  • Int32, Int64, Uint32, Uint64, Uintptr, Bool, and Pointer[T] alias typed atomics from sync/atomic.
  • ErrorGroup aliases errgroup.Group.
  • AnyPool aliases the non-generic sync.Pool; use Pool[T] for a typed pool.
  • AnyMap and AnyValue alias the non-generic sync.Map and atomic.Value; use Map[K, V] and Value[T] for typed wrappers.
  • AnySingleFlightGroup and AnySingleFlightResult alias singleflight.Group and singleflight.Result; use SingleFlightGroup[T] and SingleFlightResult[T] for typed variants.

These are type aliases rather than wrappers, so their behavior is exactly the same as the underlying type.

πŸͺ Hooks

Most execution helpers accept a sync.Hook:

  • OnRun(context.Context) error is required.
  • OnError(context.Context, error) error is optional.
  • If OnRun is nil, helpers return sync.ErrNoOnRunProvided.
  • Helpers validate OnRun before context cancellation or timeout shortcuts, so a nil OnRun returns sync.ErrNoOnRunProvided even if the context is already canceled or timeout <= 0.
  • Hook callbacks must not panic; helpers do not recover panics, pass them to OnError, or return them as errors.

OnError is only called when OnRun returns a non-nil error. If OnError returns a different error, that new error is returned.

How that returned error is observed depends on the helper:

  • Wait and Timeout return it only if OnRun finishes before their timeout/cancellation path wins.
  • Worker never returns handler errors from Schedule or TrySchedule; use OnError for logging or side effects.
package main

import (
    "context"
    "errors"
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    runErr := errors.New("boom")
    hook := sync.Hook{
        OnRun: func(context.Context) error {
            return runErr
        },
        OnError: func(_ context.Context, err error) error {
            return fmt.Errorf("wrapped: %w", err)
        },
    }

    err := hook.Error(context.Background(), hook.OnRun(context.Background()))
    fmt.Println(errors.Is(err, runErr))
}

⏱️ Wait vs Timeout

Wait and Timeout both run Hook.OnRun, but they differ:

  • Wait: best-effort wait up to timeout; returns nil on timeout/cancel and does not cancel OnRun.
  • Timeout: derives a timeout context for OnRun; returns the context cause when the context ends first (sync.ErrTimeout, context.Canceled, or a parent-provided cause).
  • If the input context is already done, Wait returns nil immediately and Timeout returns the input context's cause immediately (neither invokes OnRun).
  • If timeout <= 0, Wait returns nil immediately, while Timeout returns sync.ErrTimeout immediately.

[!IMPORTANT] Neither Wait nor Timeout forcibly stops OnRun. If the handler ignores context cancellation, it can continue running after the helper returns.

Use sync.IsTimeoutError(err) to check if an error matches sync.ErrTimeout or context.DeadlineExceeded.

⏳ Wait example (best effort)
package main

import (
    "context"
    "errors"
    "fmt"
    "time"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    err := sync.Wait(context.Background(), 10*time.Millisecond, sync.Hook{
        OnRun: func(context.Context) error {
            time.Sleep(time.Second)
            return errors.New("finished too late")
        },
    })

    // true: Wait timed out first.
    fmt.Println(err == nil)
}
🚦 Timeout example (propagated cancellation)
package main

import (
    "context"
    "fmt"
    "time"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    err := sync.Timeout(context.Background(), 10*time.Millisecond, sync.Hook{
        OnRun: func(ctx context.Context) error {
            <-ctx.Done()
            return context.Cause(ctx)
        },
    })

    fmt.Println(sync.IsTimeoutError(err))
}

πŸ‘· Worker

Worker schedules asynchronous handlers with bounded concurrency.

  • Zero value is not ready; use NewWorker(count).
  • NewWorker(count) returns a ready-to-use pointer to a worker with at most count in-flight handlers.
  • Do not copy a Worker after first use; pass and store *Worker values.
  • Schedule is context-only: it blocks until a slot is acquired or ctx is done, and does not derive or bound any deadline itself.
  • TrySchedule attempts to acquire a slot immediately and returns sync.ErrWorkerFull if capacity is unavailable.
  • The context passed to Schedule is also the context passed to OnRun; to bound only the wait for a slot, pass a ctx with a deadline to Schedule. To give the handler its own run budget starting when it actually begins, wrap ctx with context.WithTimeout (or similar) inside OnRun.
  • Schedule and TrySchedule return only scheduling errors.
  • Schedule reports context.Cause(ctx) or ErrNoOnRunProvided; TrySchedule reports the input context cause, ErrWorkerFull, or ErrNoOnRunProvided.
  • Once a handler has been scheduled, scheduling returns nil even if that handler later observes ctx.Done().
  • Wait(ctx) waits for all successfully scheduled handlers to complete, returning nil, or returns context.Cause(ctx) if ctx is done first; it does not cancel running handlers.
  • If the input context is already canceled, Schedule and TrySchedule return the input context's cause immediately and do not schedule OnRun.

[!NOTE] Worker scheduling methods report scheduling errors only. Handler errors are routed through Hook.OnError and are not returned by Schedule or TrySchedule.

If count == 0, Schedule always blocks until ctx is done and TrySchedule returns sync.ErrWorkerFull immediately.

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    worker := sync.NewWorker(4)
    defer func() {
        if err := worker.Wait(context.Background()); err != nil {
            log.Printf("wait failed: %v", err)
        }
    }()

    for i := 0; i < 3; i++ {
        job := i
        err := worker.Schedule(context.Background(), sync.Hook{
            OnRun: func(context.Context) error {
                fmt.Println("job", job)
                return nil
            },
            OnError: func(_ context.Context, err error) error {
                log.Printf("job failed: %v", err)
                return err
            },
        })
        if err != nil {
            log.Printf("schedule failed: %v", err)
        }
    }
}

⏳ Future

Async starts a typed operation immediately and returns a Future[T] for its eventual result.

  • Async runs the operation in a new goroutine.
  • The work context passed to Async controls the operation.
  • Async invokes the operation even when its work context is already canceled; the operation is responsible for observing ctx.Done().
  • Future.Await controls only how long the caller waits; canceling its context does not cancel the operation.
  • The value and error are cached, so Await can be called repeatedly by one or more callers after completion.
  • If await cancellation is selected, Await checks completion once more; a result published by that check wins, otherwise the await context's cause is returned.
  • Operations must not panic; Future does not recover panics.
package main

import (
    "context"
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    future := sync.Async(context.Background(), func(context.Context) (int, error) {
        return 42, nil
    })

    value, err := future.Await(context.Background())
    fmt.Println(value, err == nil)
}

πŸ‘₯ Group

🧩 ErrorGroup / ErrorsGroup / WaitGroup

sync.ErrorGroup is a type alias for errgroup.Group. sync.ErrorsGroup waits for every scheduled function and returns all non-nil errors joined with errors.Join. sync.WaitGroup is a type alias for sync.WaitGroup.

package main

import (
    "errors"
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    var g sync.ErrorGroup

    g.Go(func() error { return nil })
    g.Go(func() error { return errors.New("boom") })

    fmt.Println(g.Wait() != nil)
}

Use ErrorsGroup when callers need every error rather than only the first one:

ErrorsGroup retains recorded errors for its lifetime. Use a fresh ErrorsGroup for each independent batch of work. Functions passed to ErrorsGroup.Go must not panic; panics are not joined into the error returned by Wait. Call SetLimit(n) to bound how many functions run concurrently; a negative n, and the zero value, mean unbounded. SetLimit must not be called while any function started by Go is still running. Use TryGo for non-blocking, best-effort submission: it starts a function only if a concurrency slot is currently free, returning false without starting it otherwise, while still joining every error into a later Wait. Start the first function before calling Wait for an empty group, and wait for a batch to finish before starting the next independent batch. Do not copy an ErrorsGroup after first use.

package main

import (
    "errors"
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    var g sync.ErrorsGroup

    first := errors.New("first")
    second := errors.New("second")

    g.Go(func() error { return first })
    g.Go(func() error { return second })

    err := g.Wait()
    fmt.Println(errors.Is(err, first), errors.Is(err, second))
}
✈️ SingleFlightGroup

SingleFlightGroup[T] deduplicates concurrent work by key.

  • Zero value is ready for use; NewSingleFlightGroup[T]() is optional and returns a ready-to-use pointer.
  • Do(key, fn) returns (value, err, shared).
  • DoChan(key, fn) returns a receive-only channel of SingleFlightResult[T].
  • shared == true means the result was given to multiple callers.
  • On fn error, Do returns zero T plus the error, and DoChan sends a result with zero T plus the error.
  • If T is an interface type and fn returns a nil interface value, Do and DoChan expose it as zero T.
  • DoChan follows singleflight.Group.DoChan: the returned channel receives one buffered result and is not closed.
  • Completed results are not cached; Forget only affects a call that is still in flight.
  • Do not copy a SingleFlightGroup[T] after first use.
package main

import (
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    var g sync.SingleFlightGroup[int]

    v, err, shared := g.Do("key", func() (int, error) {
        return 42, nil
    })
    fmt.Println(v, err == nil, shared)
}

🏊 Pool

🧺 Generic Pool

Pool[T] is a typed wrapper around sync.Pool.

  • Stores *T values.
  • Zero value is ready for use.
  • NewPool[T]() returns a ready-to-use pointer with the default new(T) constructor.
  • Set New func() *T when values need custom initialization.
  • When New is nil, Get allocates new(T) when the pool is empty.
  • Follows normal sync.Pool semantics (runtime may drop entries anytime).
  • Does not reset values automatically on Put; callers are responsible for reuse hygiene.
  • Put(nil) is a no-op.
  • Do not copy a Pool[T] after first use.
package main

import (
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    type item struct {
        ID int
    }

    pool := sync.Pool[item]{
        New: func() *item {
            return &item{ID: 10}
        },
    }
    it := pool.Get()
    pool.Put(it)

    fmt.Println("ok")
}
🧽 BufferPool

BufferPool is a convenience wrapper over Pool[bytes.Buffer].

  • Zero value is not ready; use NewBufferPool() which returns a ready-to-use pointer.
  • Get returns *bytes.Buffer.
  • Get returns an empty buffer.
  • Put resets the buffer (nil-safe no-op).
  • Copy returns a cloned []byte (non-aliasing, nil-safe).
package main

import (
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    bp := sync.NewBufferPool()
    buf := bp.Get()
    defer bp.Put(buf)

    buf.WriteString("hello")
    out := bp.Copy(buf)
    fmt.Println(string(out))
}

βš›οΈ Value

Value[T] is a typed wrapper around atomic.Value.

  • Zero value is ready (NewValue is optional and returns a ready-to-use pointer).
  • Do not copy a Value[T] after first use.
  • Load and Swap return zero T if unset.
  • Same underlying constraints as atomic.Value apply.
  • If T is an interface type, storing or swapping a nil interface value panics just like atomic.Value.Store(nil) or atomic.Value.Swap(nil).
  • When T is an interface or any, stored and swapped values must still be consistent with atomic.Value's concrete-type rules.
  • CompareAndSwap follows atomic.Value.CompareAndSwap; non-comparable dynamic values in old can panic, and a nil interface value for new panics.
  • On an unset Value, CompareAndSwap can initialize only when old is a nil interface value; comparing against zero T does not generally initialize.
package main

import (
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    var v sync.Value[int]
    fmt.Println(v.Load())

    v.Store(1)
    fmt.Println(v.Swap(2))
    fmt.Println(v.CompareAndSwap(2, 3))
}

πŸ—ΊοΈ Map

Map[K, V] is a typed wrapper around sync.Map.

  • Zero value is ready (NewMap is optional and returns a ready-to-use pointer).
  • Do not copy a Map[K, V] after first use.
  • Load, LoadOrStore, LoadAndDelete, and Swap return zero V when needed; use boolean flags to distinguish missing keys.
  • If K is an interface type and a nil interface key is stored, Range exposes it as zero K (for example, nil for interface K).
  • If V is an interface type and a nil interface value is stored, value-returning methods expose it as zero V (for example, nil for interface V).
  • Range follows sync.Map.Range semantics and does not provide a consistent snapshot during concurrent mutation.
  • Clear removes all entries.
  • CompareAndSwap / CompareAndDelete follow sync.Map comparability rules; non-comparable dynamic old values can panic.
package main

import (
    "fmt"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    m := sync.NewMap[string, int]()

    m.Store("one", 1)
    v, ok := m.Load("one")
    fmt.Println(v, ok)

    prev, loaded := m.LoadOrStore("one", 99)
    fmt.Println(prev, loaded)

    m.Range(func(k string, v int) bool {
        fmt.Println(k, v)
        return true
    })
}

Nil interface edge-case example:

package main

import (
    "fmt"
    "io"

    "github.com/alexfalkowski/go-sync"
)

func main() {
    var m sync.Map[string, io.Reader]
    var r io.Reader

    m.Store("reader", r)
    m.Range(func(_ string, value io.Reader) bool {
        fmt.Println(value == nil)
        return true
    })
}

πŸ”Ž Background / References

This library draws inspiration from:

For executable, CI-verified usage examples, see example_test.go. Those examples back the rendered package documentation on pkg.go.dev.

Documentation ΒΆ

Overview ΒΆ

Package sync provides small concurrency helpers.

This module is github.com/alexfalkowski/go-sync and exposes package sync.

Overview ΒΆ

The package contains:

  • Convenience aliases for common synchronization primitives and atomics.
  • Hook-based helpers for running an operation with centralized error handling.
  • Wait and Timeout helpers for coordinating an operation with a timeout.
  • Worker: a bounded scheduler for running operations concurrently.
  • Future: typed asynchronous operations with context-aware waiting.
  • Group helpers built on errgroup, errors.Join, and singleflight.
  • Typed wrappers around sync.Pool, sync.Map, and sync/atomic.Value.
  • BufferPool: a convenience pool for bytes.Buffer.

The package is intentionally small. Most types are either thin wrappers over the standard library or type aliases for widely used synchronization helpers.

Aliases ΒΆ

Once, Mutex, RWMutex, and WaitGroup are aliases for their counterparts in the standard library sync package.

Int32, Int64, Uint32, Uint64, Uintptr, Bool, and Pointer[T] are aliases for atomic types from sync/atomic.

ErrorGroup is an alias for errgroup.Group. ErrorsGroup runs functions concurrently and returns all non-nil errors joined with errors.Join.

AnyPool, AnyMap, AnyValue, AnySingleFlightGroup, and AnySingleFlightResult are aliases for the non-generic sync.Pool, sync.Map, atomic.Value, singleflight.Group, and singleflight.Result; use Pool[T], Map[K,V], Value[T], SingleFlightGroup[T], and SingleFlightResult[T] for the typed wrappers.

Hooks ΒΆ

Many APIs accept a Hook. Hook.OnRun is the operation to execute and is required. Hook.OnError is optional and centralizes error handling; when set, it is invoked only when OnRun returns a non-nil error. If OnError is nil, errors are returned (or ignored) as described by the calling API. Hook.OnRun is validated before timeout or context-cancellation shortcuts, so helpers return ErrNoOnRunProvided first when OnRun is nil. Hook callbacks must not panic. Helpers do not recover panics from Hook.OnRun or Hook.OnError; a panic is not routed through Hook.OnError and is not returned as an error.

Wait and Timeout return the result of hook.Error when the operation finishes before their own deadline logic wins the race. Worker never returns handler errors from Schedule or TrySchedule; it only invokes hook.Error for side effects.

Timeouts ΒΆ

There are two timeout-related helpers with different semantics:

Wait runs hook.OnRun and waits up to the provided timeout for it to complete. After Hook.OnRun validation, if the timeout expires (or the provided context is canceled) first, Wait returns nil without waiting for OnRun to finish. This makes Wait a β€œbest effort” coordination helper rather than a cancellation mechanism. A non-positive timeout behaves the same way and returns nil without invoking Hook.OnRun.

Timeout runs hook.OnRun using a derived context with the provided timeout. After Hook.OnRun validation, if the context’s deadline expires (or it is canceled) first, Timeout returns the derived context's cancellation cause (typically ErrTimeout, context.Canceled, or a parent-provided cause). A non-positive timeout produces an already-expired derived context, so Timeout returns ErrTimeout without invoking Hook.OnRun.

In both helpers, returning from Wait or Timeout does not forcibly stop the goroutine running Hook.OnRun. If OnRun ignores context cancellation, it may continue running in the background even after the helper has returned.

In both cases, if Hook.OnRun is nil, the functions return ErrNoOnRunProvided.

Worker ΒΆ

Worker schedules hook.OnRun to run asynchronously while bounding concurrency. Schedule blocks until a slot is acquired or the provided context is done, and is context-only: it does not derive or bound any deadline itself. To bound the wait for a slot, pass a ctx with a deadline; to give the handler its own run budget starting when it actually begins, wrap ctx inside OnRun. TrySchedule attempts to schedule only if capacity is available immediately and returns ErrWorkerFull otherwise. Errors returned by OnRun are routed to hook.OnError (if set) and are not returned by either scheduling method. Use Worker.Wait to wait for all scheduled handlers to finish, or return early with the provided context's cancellation cause if the handlers have not finished first.

The zero value of Worker is not ready for use; construct one with NewWorker. A Worker must not be copied after first use; pass and store *Worker values.

Future ΒΆ

Async starts a typed operation in a new goroutine and returns a Future for its result. Future caches the operation's value and error, so Await can be called repeatedly by one or more callers after completion.

The context passed to Async is the operation's work context. Async invokes the operation even when that context is already done; the operation is responsible for observing cancellation. The context passed to Future.Await controls only how long that caller waits; cancellation of the await context does not cancel the operation. A later Await can still retrieve the eventual result. When await cancellation is selected, Await checks completion once more, so a result published by that check wins; otherwise it returns the await context's cause.

Future does not recover panics from the operation. The operation callback must not panic.

Groups ΒΆ

ErrorsGroup runs functions concurrently and waits for all of them to finish. Wait returns all non-nil errors joined with errors.Join in the order the functions were passed to Go. ErrorsGroup retains recorded errors for its lifetime, so use a fresh ErrorsGroup for each independent batch of work. SetLimit(n) optionally bounds how many functions run concurrently; a negative n, and the zero value, mean unbounded. TryGo starts a function only if a concurrency slot is currently free, returning false without starting it otherwise. Start the first function before calling Wait for an empty group, and wait for a batch to finish before starting the next independent batch. Do not copy an ErrorsGroup after first use.

SingleFlightGroup[T] is a generic wrapper around singleflight.Group. Its zero value is ready for use. Do returns typed values directly, while DoChan returns a channel of typed SingleFlightResult[T] values for select-based workflows. Both methods preserve singleflight's shared-result behavior. When T is an interface type and the function returns a nil interface value, they expose that result as the zero value of T. Do not copy a SingleFlightGroup[T] after first use.

Typed wrappers ΒΆ

Pool[T] is a typed wrapper around sync.Pool. Its zero value is ready for use. NewPool[T] returns a pointer with the default new(T) constructor. Set Pool.New when pooled values need custom initialization; if Pool.New is nil, Get allocates new(T) when the pool is empty.

BufferPool is a convenience wrapper over Pool[bytes.Buffer]. Unlike Pool[T], its zero value is not ready for use; construct one with NewBufferPool.

Value[T] is a typed wrapper around atomic.Value. Its zero value is ready for use. Load and Swap return the zero value of T if no value has been stored yet. When T is an interface type, storing a nil interface value has the same behavior as atomic.Value.Store(nil) and panics. Do not copy a Value after first use.

Map[K,V] is a typed wrapper around sync.Map. Its zero value is ready for use. If K is an interface type and a nil interface key is stored, Range exposes that entry's key as the zero value of K. If V is an interface type and a nil interface value is stored, methods that return values expose that entry as the zero value of V. Range follows the same semantics as sync.Map.Range and does not provide a consistent snapshot. Do not copy a Map after first use.

Index ΒΆ

Examples ΒΆ

Constants ΒΆ

This section is empty.

Variables ΒΆ

View Source
var ErrNoOnRunProvided = errors.New("no OnRun handler provided")

ErrNoOnRunProvided is returned when Hook.OnRun is nil.

View Source
var ErrTimeout = fmt.Errorf("timeout: %w", context.DeadlineExceeded)

ErrTimeout is the timeout cause used by derived contexts in this package.

It wraps context.DeadlineExceeded, so errors.Is matches both values.

View Source
var ErrWorkerFull = errors.New("worker has no available slot")

ErrWorkerFull is returned by Worker.TrySchedule when no concurrency slot is available immediately.

Functions ΒΆ

func IsTimeoutError ΒΆ

func IsTimeoutError(err error) bool

IsTimeoutError reports whether err matches ErrTimeout or context.DeadlineExceeded.

It uses errors.Is, so wrapped deadline-exceeded errors also report true.

func Timeout ΒΆ

func Timeout(ctx context.Context, timeout time.Duration, hook Hook) error

Timeout runs hook.OnRun with a derived context that has the given timeout.

Timeout differs from Wait in two key ways:

  1. Cancellation is propagated to OnRun by passing a derived context created by context.WithTimeoutCause. Well-behaved OnRun implementations should observe ctx.Done() and exit promptly.
  2. Timeout reports timeout/cancellation by returning context.Cause when the derived context becomes done before OnRun completes.

If OnRun completes before the derived context is done, Timeout returns hook.Error(ctx, hook.OnRun(ctx)) where ctx is the derived context.

Even after receiving the OnRun result, Timeout re-checks the derived context. If it is done at that point, Timeout returns context.Cause.

After OnRun validation, if the input ctx is already done on entry, Timeout returns its cancellation cause without invoking OnRun. If timeout <= 0, Timeout returns ErrTimeout without invoking OnRun.

As with Wait, returning from Timeout does not forcibly stop the goroutine running OnRun. If OnRun ignores ctx.Done(), it may continue running in the background. Hook.OnError may still run there, but Timeout discards its return value once the derived context has already ended. Timeout does not recover panics from OnRun or OnError; see Hook.

If hook.OnRun is nil, Timeout returns ErrNoOnRunProvided before checking whether ctx is done or timeout <= 0.

Example ΒΆ
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	err := sync.Timeout(context.Background(), 10*time.Millisecond, sync.Hook{
		OnRun: func(ctx context.Context) error {
			<-ctx.Done()
			return context.Cause(ctx)
		},
	})

	fmt.Println(sync.IsTimeoutError(err))
}
Output:
true

func Wait ΒΆ

func Wait(ctx context.Context, timeout time.Duration, hook Hook) error

Wait runs hook.OnRun and waits up to timeout for it to complete.

Wait is a β€œbest effort” waiting helper. It does not cancel the work started by OnRun. Instead, it starts OnRun asynchronously and then waits for whichever happens first:

  1. OnRun completes: Wait returns hook.Error(ctx, hook.OnRun(ctx)).
  2. The timeout elapses: Wait returns nil immediately.
  3. ctx is done: Wait returns nil immediately.

Even after receiving the OnRun result, Wait re-checks timeout/context state before returning. If timeout has elapsed or ctx is done at that point, Wait returns nil.

Important: if the timeout elapses or ctx becomes done, Wait returns without waiting for OnRun to finish. The OnRun goroutine may continue running in the background. If OnRun later returns an error, Hook.OnError may still run in that goroutine, but Wait discards the final return value. Wait does not recover panics from OnRun or OnError; see Hook.

After OnRun validation, if ctx is already done on entry (or timeout <= 0), Wait returns nil without invoking OnRun.

If hook.OnRun is nil, Wait returns ErrNoOnRunProvided before checking whether ctx is done or timeout <= 0.

Example ΒΆ
package main

import (
	"context"
	"fmt"
	"time"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	err := sync.Wait(context.Background(), time.Second, sync.Hook{
		OnRun: func(context.Context) error {
			return nil
		},
	})

	fmt.Println(err == nil)
}
Output:
true

Types ΒΆ

type AnyMap ΒΆ added in v1.29.0

type AnyMap = sync.Map

AnyMap is an alias for sync.Map.

It is provided for convenience so users of this package can refer to the non-generic map without importing `sync` directly. For a typed map keyed by K with values of type V, use Map.

type AnyPool ΒΆ added in v1.29.0

type AnyPool = sync.Pool

AnyPool is an alias for sync.Pool.

It is provided for convenience so users of this package can refer to the standard, non-generic pool without importing `sync` directly. For a typed pool that stores and returns *T without caller-side type assertions, use Pool.

type AnySingleFlightGroup ΒΆ added in v1.29.0

type AnySingleFlightGroup = singleflight.Group

AnySingleFlightGroup is an alias for singleflight.Group.

It is provided for convenience so users of this package can refer to the non-generic singleflight group without importing `golang.org/x/sync/singleflight` directly. For a type-safe group that returns values of type T, use SingleFlightGroup.

type AnySingleFlightResult ΒΆ added in v1.29.0

type AnySingleFlightResult = singleflight.Result

AnySingleFlightResult is an alias for singleflight.Result.

It is provided for convenience so users of this package can refer to the non-generic singleflight result (as returned by [AnySingleFlightGroup.DoChan]) without importing `golang.org/x/sync/singleflight` directly. Its value field is `Val any`; for a type-safe result with a `Value T` field, use SingleFlightResult.

type AnyValue ΒΆ added in v1.29.0

type AnyValue = atomic.Value

AnyValue is an alias for atomic.Value.

It is provided for convenience so users of this package can refer to the non-generic atomic value without importing `sync/atomic` directly. For a typed value holding T, use Value.

type Bool ΒΆ added in v1.15.0

type Bool = atomic.Bool

Bool is an alias for atomic.Bool.

It is provided for convenience so users of this package can refer to a typed atomic boolean without importing sync/atomic directly.

type BufferPool ΒΆ

type BufferPool struct {
	// contains filtered or unexported fields
}

BufferPool provides pooled bytes.Buffer values.

Buffers returned by BufferPool.Get should be considered temporarily borrowed by the caller. Return them to the pool via BufferPool.Put when finished to enable reuse and reduce allocations.

The zero value is not ready for use.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	pool := sync.NewBufferPool()
	buffer := pool.Get()
	defer pool.Put(buffer)

	buffer.WriteString("hello")
	copy := pool.Copy(buffer)
	fmt.Println(string(copy))
}
Output:
hello

func NewBufferPool ΒΆ

func NewBufferPool() *BufferPool

NewBufferPool returns a pointer to an initialized BufferPool.

The returned pool is ready for use and is backed by a generic Pool of bytes.Buffer values.

The zero value of BufferPool is not ready for use; construct one with NewBufferPool.

func (*BufferPool) Copy ΒΆ added in v1.1.0

func (p *BufferPool) Copy(buffer *bytes.Buffer) []byte

Copy returns a copy of the buffer contents as a new byte slice.

The returned slice does not alias the buffer's underlying array, so it is safe to keep after the buffer is returned to the pool.

If buffer is nil, Copy returns nil.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	pool := sync.NewBufferPool()
	fmt.Println(pool.Copy(nil) == nil)
}
Output:
true

func (*BufferPool) Get ΒΆ

func (p *BufferPool) Get() *bytes.Buffer

Get returns a buffer from the pool.

The returned buffer is empty. New buffers start zeroed, and BufferPool.Put resets buffers before returning them to the pool.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	pool := sync.NewBufferPool()
	buffer := pool.Get()
	defer pool.Put(buffer)

	buffer.WriteString("aaa")
	fmt.Println(buffer.String())
}
Output:
aaa

func (*BufferPool) Put ΒΆ

func (p *BufferPool) Put(buffer *bytes.Buffer)

Put resets buffer and puts it back into the pool.

If buffer is nil, Put is a no-op.

type ErrorGroup ΒΆ added in v1.9.0

type ErrorGroup = errgroup.Group

ErrorGroup is an alias for errgroup.Group.

It is provided for convenience so users of this package can refer to an errgroup without importing `golang.org/x/sync/errgroup` directly.

Note: this is a type alias, not a wrapper. All behavior, including how errors are captured and how `Wait` behaves, is defined by `errgroup.Group`.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.ErrorGroup

	g.Go(func() error { return nil })
	g.Go(func() error { return context.Canceled })

	fmt.Println(g.Wait() != nil)
}
Output:
true

type ErrorHandler ΒΆ

type ErrorHandler func(context.Context, error) error

ErrorHandler is the signature for Hook.OnError.

It is invoked only when a non-nil error is returned from Hook.OnRun. If the ErrorHandler returns a different error, Hook.Error returns that error. Whether that value reaches the API caller depends on the helper invoking the hook.

type ErrorsGroup ΒΆ added in v1.24.0

type ErrorsGroup struct {
	// contains filtered or unexported fields
}

ErrorsGroup runs functions concurrently and joins all returned errors.

Unlike ErrorGroup, which returns the first non-nil error reported by an errgroup, ErrorsGroup records every non-nil error and returns them from ErrorsGroup.Wait using errors.Join.

ErrorsGroup retains recorded errors for its lifetime. Use a fresh ErrorsGroup for each independent batch of work.

By default, ErrorsGroup places no limit on the number of concurrently running functions started by ErrorsGroup.Go. Call ErrorsGroup.SetLimit to cap concurrency.

Functions passed to ErrorsGroup.Go must not panic; panics are not recovered or joined into the error returned by ErrorsGroup.Wait.

The zero value of ErrorsGroup is ready for use.

An ErrorsGroup must not be copied after first use.

Example ΒΆ
package main

import (
	"errors"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.ErrorsGroup

	first := errors.New("first")
	second := errors.New("second")

	g.Go(func() error { return first })
	g.Go(func() error { return second })

	err := g.Wait()
	fmt.Println(errors.Is(err, first), errors.Is(err, second))
}
Output:
true true

func (*ErrorsGroup) Go ΒΆ added in v1.24.0

func (g *ErrorsGroup) Go(f func() error)

Go calls the given function in a new goroutine.

The first call to ErrorsGroup.Wait blocks until all functions started by Go have returned. Non-nil errors are joined in the order the functions were passed to Go, not the order they complete.

If a limit is set via ErrorsGroup.SetLimit, Go blocks until a concurrency slot is available before starting the function.

Go inherits sync.WaitGroup.Go sequencing constraints: start the first function before calling Wait for an empty group, and wait for a batch to finish before starting the next independent batch.

func (*ErrorsGroup) SetLimit ΒΆ added in v1.31.0

func (g *ErrorsGroup) SetLimit(n int)

SetLimit limits the number of concurrently running functions started by ErrorsGroup.Go to at most n. A negative n means unbounded, which is also the default for a zero-value ErrorsGroup. A limit of zero prevents any subsequent call to Go from starting its function, matching errgroup.Group.SetLimit.

Mirroring errgroup.Group.SetLimit, SetLimit must not be called while any function started by Go is still running; a limit set before Go is first called applies to every subsequent call to Go.

Example ΒΆ
package main

import (
	"errors"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.ErrorsGroup
	g.SetLimit(1)

	first := errors.New("first")
	second := errors.New("second")

	g.Go(func() error { return first })
	g.Go(func() error { return second })

	err := g.Wait()
	fmt.Println(errors.Is(err, first), errors.Is(err, second))
}
Output:
true true

func (*ErrorsGroup) TryGo ΒΆ added in v1.33.0

func (g *ErrorsGroup) TryGo(f func() error) bool

TryGo calls the given function in a new goroutine only if a concurrency slot is currently free.

If a limit is set via ErrorsGroup.SetLimit and no slot is free, TryGo returns false without starting f. Otherwise TryGo returns true; f runs like one started by ErrorsGroup.Go, and its error, if any, is joined into the result of a later ErrorsGroup.Wait. When no limit is set, TryGo always starts f, matching errgroup.Group.TryGo.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.ErrorsGroup
	g.SetLimit(1)

	release := make(chan struct{})
	started := g.TryGo(func() error {
		<-release
		return nil
	})

	rejected := g.TryGo(func() error { return nil })

	close(release)
	_ = g.Wait()

	fmt.Println(started, rejected)
}
Output:
true false

func (*ErrorsGroup) Wait ΒΆ added in v1.24.0

func (g *ErrorsGroup) Wait() error

Wait blocks until all functions started by ErrorsGroup.Go have returned, then returns all non-nil errors joined with errors.Join.

Wait does not clear recorded errors. A later call to Wait on the same ErrorsGroup can return errors from earlier Go calls.

type Future ΒΆ added in v1.30.0

type Future[T any] struct {
	// contains filtered or unexported fields
}

Future represents the eventual result of an asynchronous operation.

A Future is safe for concurrent use. Its result is cached, so Await can be called repeatedly by one or more callers after the operation completes. The zero value is not ready for use; construct a Future with Async. Do not copy a Future after first use; pass and store *Future values.

func Async ΒΆ added in v1.30.0

func Async[T any](ctx context.Context, fn func(context.Context) (T, error)) *Future[T]

Async starts fn in a new goroutine and returns a Future for its result.

ctx is passed to fn and controls the operation. Async invokes fn even if ctx is already done, so fn is responsible for observing cancellation. Async does not cancel the operation when a caller's Await context is done. If fn returns an error, Async caches that error and returns it from every subsequent Await call. fn must be non-nil and must not panic; Async does not recover panics from fn.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	future := sync.Async(context.Background(), func(context.Context) (int, error) {
		return 42, nil
	})

	value, err := future.Await(context.Background())
	fmt.Println(value, err == nil)
}
Output:
42 true

func (*Future[T]) Await ΒΆ added in v1.30.0

func (f *Future[T]) Await(ctx context.Context) (T, error)

Await waits for the Future to complete or for ctx to be done.

When ctx.Done is selected, Await checks completion once more before returning. A result published by that check wins; otherwise Await returns ctx's cancellation cause without canceling the operation. A later Await can still retrieve the operation's eventual result. Once the operation completes, its cached value and error are returned to every caller.

Example ΒΆ
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	release := make(chan struct{})
	future := sync.Async(context.Background(), func(context.Context) (int, error) {
		<-release
		return 42, nil
	})

	// Awaiting with an already-canceled context returns the cause without
	// canceling the still-running operation.
	canceled, cancel := context.WithCancel(context.Background())
	cancel()
	_, err := future.Await(canceled)
	fmt.Println(errors.Is(err, context.Canceled))

	// After the operation finishes, a later Await retrieves the cached result.
	close(release)
	value, err := future.Await(context.Background())
	fmt.Println(value, err == nil)
}
Output:
true
42 true

type Handler ΒΆ

type Handler func(context.Context) error

Handler is the signature for Hook.OnRun.

The provided context.Context is the context used by the operation invoking the hook (for example, the original ctx passed to Wait, or the derived timeout context created by Timeout).

type Hook ΒΆ

type Hook struct {
	OnRun   Handler
	OnError ErrorHandler
}

Hook bundles handlers used by helpers in this package.

The helpers in this package call Hook.OnRun to perform work and then pass the returned error to Hook.Error, which applies Hook.OnError if configured.

Hook.OnRun must be non-nil; otherwise operations return ErrNoOnRunProvided. Helpers validate OnRun before applying context or timeout shortcut paths. Hook callbacks must not panic. Helpers do not recover panics from OnRun or OnError; a panic is not routed through OnError and is not returned as an error.

Whether the value returned from Hook.Error is observed depends on the calling helper:

func (*Hook) Error ΒΆ

func (h *Hook) Error(ctx context.Context, err error) error

Error applies Hook.OnError when err is non-nil and OnError is set.

Otherwise, it returns err unchanged. A nil err always yields nil.

Example ΒΆ
package main

import (
	"context"
	"errors"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	runErr := errors.New("boom")
	hook := sync.Hook{
		OnError: func(_ context.Context, err error) error {
			return fmt.Errorf("wrapped: %w", err)
		},
	}

	err := hook.Error(context.Background(), runErr)
	fmt.Println(errors.Is(err, runErr))
}
Output:
true

type Int32 ΒΆ added in v1.13.0

type Int32 = atomic.Int32

Int32 is an alias for atomic.Int32.

It is provided for convenience so users of this package can refer to a typed atomic integer without importing sync/atomic directly.

type Int64 ΒΆ added in v1.19.0

type Int64 = atomic.Int64

Int64 is an alias for atomic.Int64.

It is provided for convenience so users of this package can refer to a typed atomic integer without importing sync/atomic directly.

type Map ΒΆ

type Map[K comparable, V any] struct {
	// contains filtered or unexported fields
}

Map is a typed wrapper around sync.Map.

It provides a generic API while preserving sync.Map’s concurrency properties. This includes sync.Map's iteration semantics: Map.Range does not provide a consistent snapshot when concurrent stores and deletes are happening.

Zero value ΒΆ

The zero value is ready for use.

Missing keys vs stored zero values ΒΆ

Methods such as Map.Load, Map.LoadAndDelete, and Map.Swap return the zero value of V when a key is not present. Use the returned boolean to distinguish β€œnot present” from a stored zero value.

Nil interface keys and values ΒΆ

If K is an interface type, storing a nil interface key results in an untyped nil key being stored.

Map.Range exposes such keys as the zero value of K.

Internally, sync.Map stores values as `any`. This wrapper type-asserts stored values back to V for some operations. If V is an interface type, storing a nil interface value (for example, `var r io.Reader = nil`) results in an untyped nil being stored.

Methods that return values from the map (such as Map.Load, Map.LoadOrStore, Map.LoadAndDelete, Map.Swap, and Map.Range) treat this as the zero value of V. Use the returned booleans where available to distinguish stored zero values from absent keys.

A Map must not be copied after first use.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var m sync.Map[string, int]
	m.Store("one", 1)

	v, ok := m.Load("one")
	fmt.Println(v, ok)
}
Output:
1 true

func NewMap ΒΆ

func NewMap[K comparable, V any]() *Map[K, V]

NewMap returns a pointer to a Map ready for use.

The zero value of Map is also ready for use; NewMap is purely optional.

func (*Map[K, V]) Clear ΒΆ

func (m *Map[K, V]) Clear()

Clear deletes all keys and values.

func (*Map[K, V]) CompareAndDelete ΒΆ

func (m *Map[K, V]) CompareAndDelete(key K, old V) bool

CompareAndDelete executes the compare-and-delete operation.

It follows sync.Map.CompareAndDelete semantics. If old's dynamic type is not comparable, CompareAndDelete panics.

func (*Map[K, V]) CompareAndSwap ΒΆ

func (m *Map[K, V]) CompareAndSwap(key K, old, new V) bool

CompareAndSwap executes the compare-and-swap operation.

It follows sync.Map.CompareAndSwap semantics. If old's dynamic type is not comparable, CompareAndSwap panics.

func (*Map[K, V]) Delete ΒΆ

func (m *Map[K, V]) Delete(key K)

Delete deletes the value for key.

func (*Map[K, V]) Load ΒΆ

func (m *Map[K, V]) Load(key K) (V, bool)

Load returns the value stored in the map for key.

It returns the zero value of V when the key is not present; ok reports whether the key was present.

func (*Map[K, V]) LoadAndDelete ΒΆ

func (m *Map[K, V]) LoadAndDelete(key K) (V, bool)

LoadAndDelete deletes the value for key, returning the previous value if any.

It returns the zero value of V when the key is not present; loaded reports whether the key was present.

func (*Map[K, V]) LoadOrStore ΒΆ

func (m *Map[K, V]) LoadOrStore(key K, value V) (V, bool)

LoadOrStore returns the existing value for key if present.

Otherwise, it stores and returns the given value.

The returned loaded result reports whether the value was already present.

If the stored value is nil (for example, when V is an interface type and a nil value was stored), it returns the zero value of V.

func (*Map[K, V]) Range ΒΆ

func (m *Map[K, V]) Range(f func(key K, value V) bool)

Range calls f sequentially for each key and value present in the map.

It follows sync.Map.Range semantics. In particular, Range does not necessarily correspond to any consistent snapshot of the map's contents.

If f returns false, Range stops the iteration.

If a stored key is nil (for example, when K is an interface type and a nil interface key was stored), Range passes the zero value of K to f.

If a stored value is nil (for example, when V is an interface type and a nil value was stored), Range passes the zero value of V to f.

Example ΒΆ
package main

import (
	"fmt"
	"io"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var m sync.Map[fmt.Stringer, io.Reader]
	var key fmt.Stringer
	var r io.Reader
	m.Store(key, r)

	m.Range(func(key fmt.Stringer, value io.Reader) bool {
		fmt.Println(key == nil, value == nil)
		return true
	})
}
Output:
true true

func (*Map[K, V]) Store ΒΆ

func (m *Map[K, V]) Store(key K, value V)

Store sets the value for key.

If V is an interface type and value is nil, the map stores an untyped nil. Load-like methods and Range expose this as the zero value of V.

func (*Map[K, V]) Swap ΒΆ

func (m *Map[K, V]) Swap(key K, value V) (V, bool)

Swap swaps the value for key and returns the previous value if any.

It returns the zero value of V when the key is not present; loaded reports whether the key was present.

type Mutex ΒΆ added in v1.9.0

type Mutex = sync.Mutex

Mutex is an alias of sync.Mutex.

It is provided for convenience so users of this package can refer to a mutex without importing the standard library sync package directly.

type Once ΒΆ added in v1.19.0

type Once = sync.Once

Once is an alias of sync.Once.

It is provided for convenience so users of this package can refer to a once value without importing the standard library sync package directly.

type Pointer ΒΆ added in v1.18.0

type Pointer[T any] = atomic.Pointer[T]

Pointer is an alias for atomic.Pointer.

It is provided for convenience so users of this package can refer to a typed atomic pointer without importing sync/atomic directly.

type Pool ΒΆ

type Pool[T any] struct {
	New func() *T
	// contains filtered or unexported fields
}

Pool is a typed wrapper around sync.Pool.

It stores and returns pointers to T (*T) to avoid copying large values. Pool does not reset values automatically on Put. If New is non-nil, Get calls it to create a value when the pool is empty. If New is nil, Get allocates new(T) when the pool is empty.

Zero value ΒΆ

The zero value is ready for use.

Semantics ΒΆ

Pool has the same semantics as sync.Pool:

  • Items may be dropped at any time by the runtime.
  • Items are meant to be reused to reduce allocations, not to manage resource lifetimes.
  • Values taken from the pool should be considered ephemeral and should not be assumed to be unique or to remain in the pool.

Callers are responsible for resetting any state on values before reusing them, if needed.

A Pool must not be copied after first use.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	type item struct {
		id int
	}

	var pool sync.Pool[item]
	v := pool.Get()
	v.id = 10
	pool.Put(v)

	v2 := pool.Get()
	fmt.Println(v2 != nil)
	pool.Put(v2)
}
Output:
true

func NewPool ΒΆ

func NewPool[T any]() *Pool[T]

NewPool returns a pointer to an initialized Pool for values of type T.

The returned pool creates new values on demand by allocating `new(T)` when empty.

Note: the returned Pool stores *T values. Callers should treat values obtained from Pool.Get as temporarily borrowed and return them to the pool with Pool.Put when finished.

func (*Pool[T]) Get ΒΆ

func (p *Pool[T]) Get() *T

Get returns a pointer to a T from the pool.

The returned pointer is owned by the caller until it is returned via Pool.Put. If New is set and returns nil, Get returns nil.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	type item struct {
		values []string
	}

	pool := sync.Pool[item]{
		New: func() *item {
			return &item{values: make([]string, 0, 2)}
		},
	}

	v := pool.Get()
	fmt.Println(v.values == nil, cap(v.values))
	pool.Put(v)
}
Output:
false 2

func (*Pool[T]) Put ΒΆ

func (p *Pool[T]) Put(b *T)

Put returns b to the pool.

Callers should ensure b is in an appropriate state for reuse (for example, by resetting fields) before calling Put.

If b is nil, Put is a no-op.

type RWMutex ΒΆ added in v1.9.0

type RWMutex = sync.RWMutex

RWMutex is an alias of sync.RWMutex.

It is provided for convenience so users of this package can refer to a read/write mutex without importing the standard library sync package directly.

type SingleFlightGroup ΒΆ added in v1.9.0

type SingleFlightGroup[T any] struct {
	// contains filtered or unexported fields
}

SingleFlightGroup suppresses duplicate executions of functions associated with the same key.

It is a thin, generic wrapper around singleflight.Group that provides type-safe results (via the type parameter T) while preserving singleflight semantics.

For a given key, the first caller executes the provided function and concurrent callers for the same key wait for that execution to complete and receive the same result.

The zero value of SingleFlightGroup is ready for use.

The type parameter T describes the value returned from SingleFlightGroup.Do and SingleFlightGroup.DoChan. If the function returns a non-nil error, both methods expose the zero value of T along with that error.

Implementation detail: the underlying singleflight implementation stores and returns values as `any`, so this wrapper performs a type assertion back to T. As long as the function passed to Do or DoChan returns a value of type T, the assertion will succeed.

When T is an interface type and fn returns a nil interface value, Do and DoChan expose that result as the zero value of T.

A SingleFlightGroup must not be copied after first use.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.SingleFlightGroup[int]

	v, err, shared := g.Do("key", func() (int, error) {
		return 42, nil
	})

	fmt.Println(v, err == nil, shared)
}
Output:
42 true false

func NewSingleFlightGroup ΒΆ added in v1.9.0

func NewSingleFlightGroup[T any]() *SingleFlightGroup[T]

NewSingleFlightGroup creates a pointer to a new SingleFlightGroup instance.

A SingleFlightGroup is a generic wrapper around singleflight.Group that provides type-safe results (via the type parameter T) while preserving singleflight semantics.

The zero value of SingleFlightGroup is already ready for use, so calling NewSingleFlightGroup is optional.

func (*SingleFlightGroup[T]) Do ΒΆ added in v1.9.0

func (g *SingleFlightGroup[T]) Do(key string, fn func() (T, error)) (T, error, bool)

Do executes fn for the given key, making sure that only one execution is in flight at a time for that key.

If another execution for the same key is already running, Do waits for it and returns the same results.

It returns (value, err, shared):

  • value is the successful result of fn (type T), or the zero value of T if err != nil.
  • err is the error returned by fn.
  • shared reports whether the result was given to multiple callers.

If fn returns a nil interface value and T is an interface type, value is the zero value of T.

func (*SingleFlightGroup[T]) DoChan ΒΆ added in v1.25.0

func (g *SingleFlightGroup[T]) DoChan(key string, fn func() (T, error)) <-chan SingleFlightResult[T]

DoChan is like SingleFlightGroup.Do but returns a channel that receives the result.

The returned channel is buffered with capacity 1 and is not closed, matching singleflight.Group.DoChan. If fn returns a nil interface value and T is an interface type, SingleFlightResult.Value is the zero value of T.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var g sync.SingleFlightGroup[int]

	ch := g.DoChan("key", func() (int, error) {
		return 42, nil
	})
	result := <-ch

	fmt.Println(result.Value, result.Err == nil, result.Shared)
}
Output:
42 true false

func (*SingleFlightGroup[T]) Forget ΒΆ added in v1.9.0

func (g *SingleFlightGroup[T]) Forget(key string)

Forget forgets an in-flight call for key.

Future calls to SingleFlightGroup.Do or SingleFlightGroup.DoChan with the same key will invoke their function rather than waiting for the earlier call to complete. Forget does not cancel or stop the forgotten in-flight call.

type SingleFlightResult ΒΆ added in v1.25.0

type SingleFlightResult[T any] struct {
	Value  T
	Err    error
	Shared bool
}

SingleFlightResult holds the result returned by SingleFlightGroup.DoChan.

Value is the successful result of the function, or the zero value of T when Err is non-nil. Shared reports whether the result was given to multiple callers.

type Uint32 ΒΆ added in v1.19.0

type Uint32 = atomic.Uint32

Uint32 is an alias for atomic.Uint32.

It is provided for convenience so users of this package can refer to a typed atomic integer without importing sync/atomic directly.

type Uint64 ΒΆ added in v1.19.0

type Uint64 = atomic.Uint64

Uint64 is an alias for atomic.Uint64.

It is provided for convenience so users of this package can refer to a typed atomic integer without importing sync/atomic directly.

type Uintptr ΒΆ added in v1.19.0

type Uintptr = atomic.Uintptr

Uintptr is an alias for atomic.Uintptr.

It is provided for convenience so users of this package can refer to a typed atomic integer without importing sync/atomic directly.

type Value ΒΆ

type Value[T any] struct {
	// contains filtered or unexported fields
}

Value is a typed wrapper around atomic.Value.

It provides a generic API while preserving the semantics and constraints of atomic.Value.

Zero value ΒΆ

The zero value is ready for use.

Unset values ΒΆ

If no value has been stored yet, Value.Load and Value.Swap return the zero value of T. Value.CompareAndSwap follows atomic.Value.CompareAndSwap: an unset Value can be initialized only by comparing against a nil interface value.

Type safety and panics ΒΆ

Internally, atomic.Value stores values as `any`. This wrapper type-asserts the stored value back to T on Load/Swap. The assertion will succeed as long as you only store values of type T in this Value.

Storing or swapping values of different concrete types in the same underlying atomic.Value has the same constraints as atomic.Value itself and may panic.

When T is an interface type, storing a nil interface value has the same behavior as atomic.Value.Store(nil) and panics.

A Value must not be copied after first use.

Example ΒΆ
package main

import (
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	var value sync.Value[int]
	fmt.Println(value.Load())

	value.Store(1)
	fmt.Println(value.Swap(2))
}
Output:
0
1

func NewValue ΒΆ added in v0.25.0

func NewValue[T any]() *Value[T]

NewValue returns a pointer to a new Value wrapper.

The returned pointer is ready for use.

func (*Value[T]) CompareAndSwap ΒΆ

func (v *Value[T]) CompareAndSwap(old, new T) bool

CompareAndSwap executes the atomic compare-and-swap operation.

It follows atomic.Value.CompareAndSwap semantics. If old's dynamic type is not comparable, CompareAndSwap panics. As with Value.Store, interface-typed values must also satisfy atomic.Value's nil and concrete-type rules. In particular, CompareAndSwap with a nil interface value for new panics.

If no value has been stored yet, CompareAndSwap can initialize the Value only when old is a nil interface value. Comparing against T's zero value returns false when that zero value is converted to a non-nil interface, such as a zero number or typed nil pointer.

func (*Value[T]) Load ΒΆ

func (v *Value[T]) Load() T

Load returns the stored value.

If no value has been stored yet, it returns the zero value of T.

func (*Value[T]) Store ΒΆ

func (v *Value[T]) Store(value T)

Store atomically stores value.

It follows atomic.Value.Store semantics. In particular, storing a nil interface value panics, and later stores must remain compatible with the concrete type established by the first store.

func (*Value[T]) Swap ΒΆ

func (v *Value[T]) Swap(new T) T

Swap atomically stores new and returns the previous value.

If no value has been stored yet, it returns the zero value of T.

It follows atomic.Value.Swap semantics. In particular, swapping a nil interface value panics, and later swaps must remain compatible with the concrete type established by the first store or swap.

type WaitGroup ΒΆ added in v1.13.0

type WaitGroup = sync.WaitGroup

WaitGroup is an alias for sync.WaitGroup.

It is provided for convenience so users of this package can refer to a WaitGroup without importing `sync` directly.

type Worker ΒΆ

type Worker struct {
	// contains filtered or unexported fields
}

Worker schedules handlers with a bounded level of concurrency.

Work is scheduled via Worker.Schedule or Worker.TrySchedule, and completion is observed via Worker.Wait. Scheduled handlers run asynchronously in their own goroutines.

The zero value is not ready for use. A Worker must not be copied after first use; pass and store *Worker values.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	worker := sync.NewWorker(2)
	var count sync.Int32

	for range 3 {
		err := worker.Schedule(context.Background(), sync.Hook{
			OnRun: func(context.Context) error {
				count.Add(1)
				return nil
			},
		})
		if err != nil {
			fmt.Println(err)
			return
		}
	}

	if err := worker.Wait(context.Background()); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count.Load())
}
Output:
3

func NewWorker ΒΆ

func NewWorker(count uint) *Worker

NewWorker returns a pointer to a Worker that bounds concurrent execution to count.

The worker uses a buffered channel of size count as a semaphore. A call to Worker.Schedule or Worker.TrySchedule acquires one slot before starting work and releases it when the work completes.

If count is 0, Worker.Schedule always blocks until the provided context times out or is canceled, and Worker.TrySchedule returns ErrWorkerFull immediately.

The zero value of Worker is not ready for use; construct one with NewWorker.

func (*Worker) Schedule ΒΆ

func (w *Worker) Schedule(ctx context.Context, hook Hook) error

Schedule attempts to schedule hook.OnRun to run asynchronously, subject to the worker's concurrency limit.

Schedule blocks until one of the following occurs:

  1. A concurrency slot is acquired: Schedule starts OnRun in a goroutine and returns nil.
  2. ctx is done first: Schedule returns context.Cause(ctx).

The context passed to OnRun is the ctx provided to Schedule. This context is also passed to hook.OnError (via hook.Error) if OnRun returns a non-nil error. Schedule does not derive or bound any deadline itself; to bound the wait for a slot, pass a ctx with a deadline. To give the handler its own run budget starting when it actually begins, wrap ctx with context.WithTimeout (or similar) inside OnRun.

Error handling semantics:

  • If hook.OnRun is nil, Schedule returns ErrNoOnRunProvided. This validation happens before the context shortcut check.
  • If the input context is already done on entry, Schedule returns its cancellation cause without scheduling OnRun.
  • Errors returned from OnRun are routed to hook.OnError (if set) and are not returned from Schedule. Schedule only reports errors related to scheduling (cancellation before a slot is acquired).
  • Once a handler has been scheduled successfully, Schedule returns nil even if ctx later expires while the handler is still running.
  • Panics from OnRun or OnError are not recovered; see Hook.

To wait for all scheduled handlers to complete, call Worker.Wait.

func (*Worker) TrySchedule ΒΆ added in v1.26.0

func (w *Worker) TrySchedule(ctx context.Context, hook Hook) error

TrySchedule attempts to schedule hook.OnRun immediately.

If a concurrency slot is available, TrySchedule starts OnRun in a goroutine and returns nil. The context passed to OnRun is the ctx provided to TrySchedule. This context is also passed to hook.OnError (via hook.Error) if OnRun returns a non-nil error.

TrySchedule does not wait for capacity. If no concurrency slot is available immediately, it returns ErrWorkerFull without scheduling OnRun.

Error handling semantics:

  • If hook.OnRun is nil, TrySchedule returns ErrNoOnRunProvided. This validation happens before context or capacity shortcut checks.
  • If the input context is already done on entry, TrySchedule returns its cancellation cause without scheduling OnRun.
  • Errors returned from OnRun are routed to hook.OnError (if set) and are not returned from TrySchedule. TrySchedule only reports scheduling errors.
  • Once a handler has been scheduled successfully, TrySchedule returns nil even if the context is later canceled while the handler is still running.
  • Panics from OnRun or OnError are not recovered; see Hook.

To wait for all scheduled handlers to complete, call Worker.Wait.

Example ΒΆ
package main

import (
	"context"
	"fmt"

	"github.com/alexfalkowski/go-sync"
)

func main() {
	worker := sync.NewWorker(1)
	var count sync.Int32

	err := worker.TrySchedule(context.Background(), sync.Hook{
		OnRun: func(context.Context) error {
			count.Add(1)
			return nil
		},
	})
	if err != nil {
		fmt.Println(err)
		return
	}

	if err := worker.Wait(context.Background()); err != nil {
		fmt.Println(err)
		return
	}
	fmt.Println(count.Load())
}
Output:
1

func (*Worker) Wait ΒΆ

func (w *Worker) Wait(ctx context.Context) error

Wait waits for all handlers that have been successfully scheduled to complete, or for ctx to be done first.

Wait returns nil once every scheduled handler has finished, or context.Cause(ctx) if ctx is done first. Completion is observed by a goroutine started for this call rather than a persistent signal, so this is a best-effort race: it narrows, but does not eliminate, the window where an already-done ctx wins over handlers that finished shortly before Wait was called. Wait never cancels a running handler; cancellation is controlled by the contexts provided to Worker.Schedule or Worker.TrySchedule and observed by the handlers themselves. A handler that ignores its context can keep running after Wait returns; the internal goroutine started by Wait also lives until that handler finishes, matching the lifetime of the already-outstanding work. Wait can be called multiple times; each call waits for the currently scheduled work to finish.

Directories ΒΆ

Path Synopsis
internal

Jump to

Keyboard shortcuts

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