Documentation
¶
Index ¶
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type ErrAcquireCancelled ¶ added in v1.1.0
type ErrAcquireCancelled struct{ Cause error }
ErrAcquireCancelled indicates that a context-aware acquire or wait was terminated because the context was cancelled or its deadline expired before the operation could complete. It wraps the underlying context error and implements errors.Unwrap, so callers can use errors.Is(err, context.Canceled) or errors.Is(err, context.DeadlineExceeded) to distinguish the cause.
- Cause: the underlying context error (context.Canceled or context.DeadlineExceeded).
func (ErrAcquireCancelled) Error ¶ added in v1.1.0
func (e ErrAcquireCancelled) Error() string
Error implements the [error] interface.
func (ErrAcquireCancelled) Is ¶ added in v1.1.0
func (e ErrAcquireCancelled) Is(target error) bool
Is supports matching via errors.Is by type.
func (ErrAcquireCancelled) String ¶ added in v1.1.0
func (e ErrAcquireCancelled) String() string
ErrAcquireCancelled is returned by context-aware acquire methods ([semaphore.AcquireWith], [semaphore.AcquireNWith], [semaphore.AcquireTimeout], etc.) when the context is cancelled or its deadline expires before a slot can be acquired.
The Cause field holds the underlying context error (typically context.Canceled or context.DeadlineExceeded).
ErrAcquireCancelled implements errors.Unwrap, so the cause can be inspected with errors.Is or errors.As.
Example:
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond)
defer cancel()
time.Sleep(5 * time.Millisecond)
if err := sem.AcquireWith(ctx); err != nil {
var acqErr sema.ErrAcquireCancelled
if errors.As(err, &acqErr) {
fmt.Printf("acquire cancelled: %v\n", acqErr.Cause)
}
if errors.Is(err, context.DeadlineExceeded) {
fmt.Println("it was a timeout")
}
}
func (ErrAcquireCancelled) Unwrap ¶ added in v1.1.0
func (e ErrAcquireCancelled) Unwrap() error
Unwrap returns the underlying context error, enabling use with errors.Is and errors.As for matching context.Canceled or context.DeadlineExceeded.
type ErrDrain ¶ added in v1.1.0
type ErrDrain struct{ Cause string }
ErrDrain indicates that Semaphore.Drain could not fully empty the internal channel. This is an unexpected internal condition and should not occur under normal usage.
- Cause: a human-readable description of what went wrong.
func (ErrDrain) String ¶ added in v1.1.0
ErrDrain is returned by [semaphore.Drain] when the internal channel could not be fully emptied. The Cause field contains a human-readable description of what went wrong.
Example:
if err := sem.Drain(); err != nil {
var drainErr sema.ErrDrain
if errors.As(err, &drainErr) {
fmt.Printf("drain problem: %s\n", drainErr.Cause)
}
}
type ErrInvalidCap ¶ added in v1.1.0
type ErrInvalidCap struct{ Value int }
ErrInvalidCap indicates that an invalid capacity was passed to New, NewWithObserver, Must, or Semaphore.SetCap. Valid capacities are any integer >= 1, or the special value -1 which selects [defaultCap].
- Value: the invalid capacity that was provided.
func (ErrInvalidCap) Error ¶ added in v1.1.0
func (e ErrInvalidCap) Error() string
Error implements the [error] interface.
func (ErrInvalidCap) Is ¶ added in v1.1.0
func (e ErrInvalidCap) Is(target error) bool
Is supports matching via errors.Is by type. Any ErrInvalidCap matches any other ErrInvalidCap regardless of the Value field.
func (ErrInvalidCap) String ¶ added in v1.1.0
func (e ErrInvalidCap) String() string
ErrInvalidCap is returned by [semaphore.SetCap] when the requested capacity is less than 1 and not the special value -1 (which resets to [defaultCap]).
The Value field contains the invalid capacity that was passed.
Example:
if err := sem.SetCap(0); err != nil {
var capErr sema.ErrInvalidCap
if errors.As(err, &capErr) {
fmt.Printf("bad capacity: %d\n", capErr.Value)
}
}
type ErrInvalidN ¶ added in v1.1.0
type ErrInvalidN struct{ Value int }
ErrInvalidN indicates that an invalid slot count (n < 1) was passed to a multi-slot method such as Semaphore.AcquireN, Semaphore.ReleaseN, or their variants.
- Value: the invalid n that was provided.
func (ErrInvalidN) Error ¶ added in v1.1.0
func (e ErrInvalidN) Error() string
Error implements the [error] interface.
func (ErrInvalidN) Is ¶ added in v1.1.0
func (e ErrInvalidN) Is(target error) bool
Is supports matching via errors.Is by type.
func (ErrInvalidN) String ¶ added in v1.1.0
func (e ErrInvalidN) String() string
ErrInvalidN is returned by [semaphore.AcquireN], [semaphore.ReleaseN], and related multi-slot methods when n is less than 1.
The Value field contains the invalid n that was passed.
Example:
if err := sem.AcquireN(0); err != nil {
var nErr sema.ErrInvalidN
if errors.As(err, &nErr) {
fmt.Printf("invalid n: %d\n", nErr.Value)
}
}
type ErrNExceedsCap ¶ added in v1.1.0
ErrNExceedsCap indicates that a multi-slot acquire was rejected because the requested count exceeds the semaphore's capacity. Allowing the acquire would deadlock, since there can never be enough slots to satisfy the request.
- Requested: the number of slots the caller asked for.
- Cap: the semaphore's capacity at the time of the call.
func (ErrNExceedsCap) Error ¶ added in v1.1.0
func (e ErrNExceedsCap) Error() string
Error implements the [error] interface.
func (ErrNExceedsCap) Is ¶ added in v1.1.0
func (e ErrNExceedsCap) Is(target error) bool
Is supports matching via errors.Is by type.
func (ErrNExceedsCap) String ¶ added in v1.1.0
func (e ErrNExceedsCap) String() string
ErrNExceedsCap is returned by [semaphore.AcquireN], [semaphore.AcquireNWith], and related multi-slot methods when the requested count n exceeds the semaphore's current capacity. Acquiring more slots than the capacity would block forever, so the call is rejected immediately.
- Requested: the number of slots the caller asked for.
- Cap: the semaphore's capacity at the time of the call.
Example:
sem := sema.New(3)
if err := sem.AcquireN(5); err != nil {
var excErr sema.ErrNExceedsCap
if errors.As(err, &excErr) {
fmt.Printf("requested %d but cap is only %d\n",
excErr.Requested, excErr.Cap)
}
}
type ErrNoSlot ¶ added in v1.1.0
ErrNoSlot indicates that a non-blocking acquire (Semaphore.TryAcquireWith or Semaphore.TryAcquireNWith) could not claim the requested number of slots because the semaphore did not have enough free capacity at the moment of the attempt.
- Requested: the number of slots the caller asked for.
- Available: the number of free slots at the time of the attempt.
func (ErrNoSlot) String ¶ added in v1.1.0
ErrNoSlot is returned by [semaphore.TryAcquireWith] and [semaphore.TryAcquireNWith] when the semaphore does not have enough free slots to satisfy a non-blocking acquire.
- Requested: the number of slots the caller asked for.
- Available: the number of free slots at the time of the attempt.
Example:
err := sem.TryAcquireNWith(ctx, 3)
var slotErr sema.ErrNoSlot
if errors.As(err, &slotErr) {
fmt.Printf("wanted %d slots, only %d free\n",
slotErr.Requested, slotErr.Available)
}
type ErrRecovered ¶ added in v1.1.0
ErrRecovered wraps a panic that was caught and converted to an error during a multi-slot acquire operation. It preserves both the raw panic value (which may be any type) and, when possible, an error-typed version for use with errors.Unwrap, errors.Is, and errors.As.
- Cause: the raw value recovered from the panic (any type).
- AsError: the Cause cast to error, or nil if Cause does not implement the error interface. Used by the Unwrap method.
func (ErrRecovered) Error ¶ added in v1.1.0
func (e ErrRecovered) Error() string
Error implements the [error] interface.
func (ErrRecovered) Is ¶ added in v1.1.0
func (e ErrRecovered) Is(target error) bool
Is supports matching via errors.Is by type.
func (ErrRecovered) String ¶ added in v1.1.0
func (e ErrRecovered) String() string
ErrRecovered is returned when [semaphore.AcquireN] (or a related method) catches a panic during execution and wraps it as an error.
- Cause: the raw value recovered from the panic (interface{}).
- AsError: the Cause converted to an error, if possible, for use with errors.Unwrap.
ErrRecovered implements errors.Unwrap via AsError, so callers can use errors.Is and errors.As to inspect the original panic value when it satisfies the error interface.
Example:
if err := sem.AcquireN(2); err != nil {
var recErr sema.ErrRecovered
if errors.As(err, &recErr) {
fmt.Printf("panic recovered: %v\n", recErr.Cause)
}
}
type ErrReleaseExceedsCount ¶ added in v1.1.0
ErrReleaseExceedsCount indicates that a Release or ReleaseN call attempted to free more slots than are currently held. This typically signals a mismatched acquire/release pair, a double release, or a release after Semaphore.Drain / Semaphore.Reset has already cleared the slots.
- Attempted: the number of slots the caller tried to release.
- Current: the number of slots actually held at call time.
func (ErrReleaseExceedsCount) Error ¶ added in v1.1.0
func (e ErrReleaseExceedsCount) Error() string
Error implements the [error] interface.
func (ErrReleaseExceedsCount) Is ¶ added in v1.1.0
func (e ErrReleaseExceedsCount) Is(target error) bool
Is supports matching via errors.Is by type.
func (ErrReleaseExceedsCount) String ¶ added in v1.1.0
func (e ErrReleaseExceedsCount) String() string
ErrReleaseExceedsCount is returned by [semaphore.Release] and [semaphore.ReleaseN] when the caller attempts to release more slots than are currently held. This typically indicates a mismatched acquire/release pair, or a release after [semaphore.Drain] or [semaphore.Reset] has already cleared the slots.
- Attempted: the number of slots the caller tried to release.
- Current: the number of slots actually held at the time of the call.
Example:
sem := sema.New(5)
// No slots acquired — releasing is an error.
if err := sem.Release(); err != nil {
var relErr sema.ErrReleaseExceedsCount
if errors.As(err, &relErr) {
fmt.Printf("tried to release %d, but only %d held\n",
relErr.Attempted, relErr.Current)
}
}
type Observer ¶ added in v1.1.0
type Observer interface {
// OnAcquire is called immediately after a slot is successfully
// acquired by any of the acquire methods ([semaphore.Acquire],
// [semaphore.AcquireWith], [semaphore.TryAcquire],
// [semaphore.AcquireN], etc.).
//
// Parameters:
// - count: the number of slots currently held after this
// acquisition (equivalent to [semaphore.Len] at call time).
// - cap: the total capacity of the semaphore (equivalent to
// [semaphore.Cap] at call time).
//
// The ratio count/cap gives the instantaneous utilization at the
// moment of acquisition. Note that for bulk acquires via
// [semaphore.AcquireN], OnAcquire is called once after all n slots
// have been claimed, not once per slot.
//
// Example usage inside an implementation:
//
// func (o *myObserver) OnAcquire(count, cap int) {
// utilization := float64(count) / float64(cap)
// o.gauge.Set(utilization)
// }
OnAcquire(count, cap int)
// OnRelease is called immediately after one or more slots are
// successfully released by [semaphore.Release] or
// [semaphore.ReleaseN].
//
// Parameters:
// - count: the number of slots still held after this release
// (equivalent to [semaphore.Len] at call time).
// - cap: the total capacity of the semaphore (equivalent to
// [semaphore.Cap] at call time).
//
// Like OnAcquire, for bulk releases via [semaphore.ReleaseN],
// OnRelease is called once after all n slots have been freed, not
// once per slot.
//
// Example usage inside an implementation:
//
// func (o *myObserver) OnRelease(count, cap int) {
// if count == 0 {
// o.logger.Println("semaphore fully idle")
// }
// }
OnRelease(count, cap int)
// OnWaitStart is called at the beginning of [semaphore.Wait],
// before the semaphore checks whether it is already empty. This
// allows observers to track how often callers enter a wait state
// and to measure wait duration by pairing with [Observer.OnWaitEnd].
//
// OnWaitStart receives no parameters because the wait has not yet
// inspected semaphore state — use the count and cap values from
// [Observer.OnAcquire] or [Observer.OnRelease] for utilization data.
//
// Example usage inside an implementation:
//
// func (o *myObserver) OnWaitStart() {
// o.waitTimer = time.Now()
// o.waitCount.Inc()
// }
OnWaitStart()
// OnWaitEnd is called when [semaphore.Wait] returns, whether it
// completed successfully (all slots drained) or was cancelled by
// the context.
//
// Parameters:
// - err: nil if the semaphore reached zero occupancy, or an
// [ErrAcquireCancelled] if the context was cancelled or its
// deadline expired before the semaphore became empty.
//
// By pairing OnWaitEnd with [Observer.OnWaitStart], observers can
// measure the wall-clock duration of the wait and distinguish
// successful drains from cancellations.
//
// Example usage inside an implementation:
//
// func (o *myObserver) OnWaitEnd(err error) {
// elapsed := time.Since(o.waitTimer)
// o.waitDuration.Observe(elapsed.Seconds())
// if err != nil {
// o.waitCancellations.Inc()
// }
// }
OnWaitEnd(err error)
}
Observer is an interface for receiving lifecycle event notifications from a Semaphore. Implementations can use these callbacks to collect metrics, emit structured logs, trigger alerts, or drive adaptive concurrency control — without modifying the semaphore itself.
An Observer is attached at construction time via NewWithObserver. A semaphore created with New or Must has no observer and incurs no callback overhead.
Concurrency and performance contract ¶
All Observer methods are called synchronously while the semaphore's internal lock is held. Implementations MUST return immediately and MUST NOT:
- Acquire additional locks that could create a lock-ordering cycle with the semaphore's own mutex.
- Call back into the same semaphore (e.g. Acquire, Release, SetCap), which would deadlock.
- Perform blocking I/O, network calls, or any operation with unbounded latency.
If you need to perform expensive work in response to an event, buffer it in a channel or queue and process it asynchronously.
Method summary ¶
- Observer.OnAcquire — a slot was successfully acquired.
- Observer.OnRelease — a slot was successfully released.
- Observer.OnWaitStart — a call to [semaphore.Wait] has begun.
- Observer.OnWaitEnd — a call to [semaphore.Wait] has finished.
Examples ¶
A minimal no-op observer (useful as a base for partial implementations):
type noopObserver struct{}
func (noopObserver) OnAcquire(count, cap int) {}
func (noopObserver) OnRelease(count, cap int) {}
func (noopObserver) OnWaitStart() {}
func (noopObserver) OnWaitEnd(err error) {}
A Prometheus-style metrics observer:
type prometheusObserver struct {
acquireTotal prometheus.Counter
releaseTotal prometheus.Counter
utilization prometheus.Gauge
}
func (p *prometheusObserver) OnAcquire(count, cap int) {
p.acquireTotal.Inc()
p.utilization.Set(float64(count) / float64(cap))
}
func (p *prometheusObserver) OnRelease(count, cap int) {
p.releaseTotal.Inc()
p.utilization.Set(float64(count) / float64(cap))
}
func (p *prometheusObserver) OnWaitStart() {}
func (p *prometheusObserver) OnWaitEnd(err error) {}
An observer that buffers events for asynchronous processing:
type asyncObserver struct {
events chan Event
}
func (a *asyncObserver) OnAcquire(count, cap int) {
select {
case a.events <- Event{Kind: "acquire", Count: count, Cap: cap}:
default:
// Drop event if buffer is full — never block.
}
}
func (a *asyncObserver) OnRelease(count, cap int) {
select {
case a.events <- Event{Kind: "release", Count: count, Cap: cap}:
default:
}
}
func (a *asyncObserver) OnWaitStart() {}
func (a *asyncObserver) OnWaitEnd(err error) {}
type Semaphore ¶
type Semaphore interface {
// Acquire blocks until a slot is available, then claims it. It waits
// indefinitely if the semaphore is full. Every successful Acquire
// must be paired with a [Semaphore.Release] call, typically via defer.
// Use [Semaphore.AcquireWith] or [Semaphore.AcquireTimeout] when
// cancellation or a deadline is needed.
Acquire()
// AcquireWith blocks until a slot is available or the provided context
// is cancelled. It returns nil on success, or [ErrAcquireCancelled]
// wrapping the context error if the context fires first. The underlying
// cause is accessible via [errors.Unwrap] or [errors.Is] against
// [context.Canceled] / [context.DeadlineExceeded].
AcquireWith(ctx context.Context) error
// AcquireTimeout blocks until a slot is available or the given duration
// elapses. It is a convenience wrapper around [Semaphore.AcquireWith]
// with an internally created [context.WithTimeout]. Returns nil on
// success, or [ErrAcquireCancelled] on timeout.
AcquireTimeout(d time.Duration) error
// TryAcquire attempts to claim a slot without blocking. It returns
// true if a slot was acquired (caller must later call
// [Semaphore.Release]), or false if the semaphore is currently full.
// Useful for fast-path checks, load shedding, or fallback logic
// where blocking is unacceptable.
TryAcquire() bool
// TryAcquireWith performs a non-blocking acquire after first checking
// whether the context is already done. It returns nil on success,
// [ErrAcquireCancelled] if the context is already cancelled, or
// [ErrNoSlot] if the semaphore is full. Unlike [Semaphore.AcquireWith],
// it never blocks waiting for a slot to become available.
TryAcquireWith(ctx context.Context) error
// Release frees a single held slot back to the semaphore, waking one
// or more blocked acquires. It returns [ErrReleaseExceedsCount] if no
// slots are currently held, indicating a mismatched acquire/release
// pair or a release after [Semaphore.Drain] / [Semaphore.Reset].
// The EWMA utilization metric is updated on each successful release.
Release() error
// AcquireN acquires n slots from the semaphore, blocking until all n
// are available. It first attempts a fast non-blocking path and falls
// back to [Semaphore.AcquireNWith] with a 10-minute internal timeout.
// Returns [ErrNExceedsCap] if n exceeds the capacity (which would
// deadlock), or [ErrInvalidN] if n < 1. Partial acquisitions are
// rolled back automatically on failure.
AcquireN(n int) error
// AcquireNWith acquires n slots, blocking until all are claimed or the
// context is cancelled. If the context fires after some but not all
// slots have been acquired, the partially acquired slots are released
// automatically. Returns [ErrAcquireCancelled] on context cancellation,
// [ErrNExceedsCap] if n exceeds capacity, or [ErrInvalidN] if n < 1.
AcquireNWith(ctx context.Context, n int) error
// AcquireNTimeout acquires n slots, blocking until all are claimed or
// the given duration elapses. It is a convenience wrapper around
// [Semaphore.AcquireNWith] with an internally created timeout context.
// Automatic rollback of partial acquisitions applies on timeout.
AcquireNTimeout(n int, d time.Duration) error
// TryAcquireN attempts to acquire n slots without blocking. It returns
// true only if all n slots were claimed atomically; if fewer than n
// slots are free, no slots are acquired and it returns false. Returns
// false for invalid n values (n < 1) or if n exceeds the capacity.
TryAcquireN(n int) bool
// TryAcquireNWith performs a non-blocking bulk acquire after checking
// whether the context is already done. It returns nil if all n slots
// were claimed, [ErrAcquireCancelled] if the context is done,
// [ErrNExceedsCap] if n exceeds capacity, [ErrInvalidN] if n < 1, or
// [ErrNoSlot] if not enough slots are currently free.
TryAcquireNWith(ctx context.Context, n int) error
// ReleaseN frees n held slots back to the semaphore. It returns
// [ErrReleaseExceedsCount] if n exceeds the number of currently held
// slots, or [ErrInvalidN] if n < 1. All blocked acquires are woken
// after a successful release, and the EWMA utilization metric is
// updated.
ReleaseN(n int) error
// Wait blocks until the semaphore is completely empty (zero slots held)
// or the context is cancelled. It returns nil when all slots have been
// released, or [ErrAcquireCancelled] if the context fires first. Wait
// does not prevent new acquires — coordinate with application-level
// stop signals to ensure no new work is dispatched while waiting.
Wait(ctx context.Context) error
// Drain forcibly removes all held slots from the internal channel.
// Goroutines that previously acquired slots will receive
// [ErrReleaseExceedsCount] when they subsequently call
// [Semaphore.Release]. Use [Semaphore.Wait] to let in-flight work
// finish gracefully before calling Drain. The EWMA utilization metric
// is reset to zero. Returns [ErrDrain] if the channel could not be
// fully emptied.
Drain() error
// Reset replaces the internal channel with a fresh one of the same
// capacity, discarding all held slots and resetting the EWMA metric
// to zero. Like [Semaphore.Drain], goroutines holding slots from
// before the reset will receive [ErrReleaseExceedsCount] on their
// next [Semaphore.Release] call. All blocked acquires are woken and
// will compete for slots on the new channel.
Reset() error
// Len returns the number of slots currently held (acquired but not
// yet released). This is a point-in-time snapshot and may be stale
// by the time the caller acts on it. Suitable for monitoring and
// logging, not for synchronization decisions.
Len() int
// Cap returns the total capacity of the semaphore — the maximum
// number of slots that may be held concurrently. Use
// [Semaphore.SetCap] to adjust this at runtime.
Cap() int
// Utilization returns the instantaneous utilization as a float64 in
// [0.0, 1.0], computed as Len / Cap. Returns 0 if the capacity is
// zero. For a temporally smoothed metric, see
// [Semaphore.UtilizationSmoothed].
Utilization() float64
// UtilizationSmoothed returns the exponentially weighted moving
// average (EWMA) of utilization as a float64 in [0.0, 1.0]. The
// EWMA is updated on every [Semaphore.Release] and
// [Semaphore.ReleaseN] call, providing a stable view of usage over
// time that is useful for adaptive concurrency control and capacity
// planning decisions.
UtilizationSmoothed() float64
// IsEmpty reports whether the semaphore has zero slots currently
// held (Len == 0). Like [Semaphore.Len], this is a snapshot and
// may change immediately after the call returns.
IsEmpty() bool
// IsFull reports whether all slots are currently held (Len == Cap),
// meaning any new [Semaphore.Acquire] call would block and any
// [Semaphore.TryAcquire] call would return false. This is a
// snapshot and may change immediately after the call returns.
IsFull() bool
// SetCap dynamically adjusts the semaphore's capacity to c. Pass -1
// to reset to [defaultCap]; any other value less than 1 returns
// [ErrInvalidCap]. If the new capacity is greater than or equal to
// the current occupancy, all held slots are preserved. If smaller,
// existing slots are drained (holders will receive
// [ErrReleaseExceedsCount] on their next release). All blocked
// acquires are re-evaluated after the resize.
SetCap(c int) error
}
Semaphore is a concurrency primitive that limits the number of goroutines accessing a shared resource or performing work simultaneously. It operates as a counting semaphore backed by a channel, with support for context-aware acquisition, bulk acquire/release, dynamic capacity changes, graceful draining, and real-time utilization metrics.
Create a Semaphore with New, NewWithObserver, or Must.
Core concept ¶
A Semaphore manages a fixed pool of slots. A goroutine claims a slot by acquiring it and returns the slot by releasing it. When all slots are held, further acquires block (or fail, for try-variants) until a slot is freed. The number of slots is the semaphore's capacity, which can be adjusted at runtime via Semaphore.SetCap.
Concurrency safety ¶
All methods on Semaphore are safe for concurrent use from multiple goroutines. The implementation uses a combination of a mutex, condition variable, and atomic operations to coordinate access.
Method groups ¶
The interface is organized into five groups:
## Single-slot acquisition
These methods acquire or attempt to acquire exactly one slot:
- Semaphore.Acquire — blocks indefinitely until a slot is available.
- Semaphore.AcquireWith — blocks until a slot is available or the context is cancelled, returning ErrAcquireCancelled on cancellation.
- Semaphore.AcquireTimeout — convenience wrapper around AcquireWith with a time.Duration deadline.
- Semaphore.TryAcquire — non-blocking; returns true if a slot was claimed, false otherwise.
- Semaphore.TryAcquireWith — non-blocking with a context check; returns ErrNoSlot if no slot is free, or ErrAcquireCancelled if the context is already done.
- Semaphore.Release — frees one held slot; returns ErrReleaseExceedsCount if no slots are currently held.
## Multi-slot (bulk) acquisition
These methods acquire or attempt to acquire n slots at once. Partial acquisitions are rolled back automatically on failure or cancellation, so the semaphore is never left in a half-acquired state:
- Semaphore.AcquireN — blocks until n slots are available (with a 10-minute internal timeout), or returns ErrNExceedsCap if n exceeds the capacity.
- Semaphore.AcquireNWith — blocks until n slots are available or the context fires.
- Semaphore.AcquireNTimeout — convenience wrapper around AcquireNWith with a time.Duration deadline.
- Semaphore.TryAcquireN — non-blocking; returns true only if all n slots were claimed atomically.
- Semaphore.TryAcquireNWith — non-blocking with a context check.
- Semaphore.ReleaseN — frees n held slots; returns ErrReleaseExceedsCount if n exceeds the current occupancy.
## Waiting and draining
These methods support graceful shutdown and lifecycle management:
- Semaphore.Wait — blocks until the semaphore is completely empty (all slots released) or the context is cancelled. Useful for waiting on in-flight work before shutting down.
- Semaphore.Drain — forcibly removes all held slots from the internal channel. Goroutines that previously acquired slots will receive ErrReleaseExceedsCount on their next Release call. Prefer calling Wait first to let in-flight work finish gracefully.
- Semaphore.Reset — replaces the internal channel with a fresh one of the same capacity, discarding all held slots. Same caveats as Drain regarding in-flight goroutines.
## Introspection
These methods provide read-only snapshots of the semaphore's state. Because the semaphore is concurrent, returned values may be stale by the time the caller acts on them — use them for monitoring, logging, and heuristics rather than synchronization decisions:
- Semaphore.Len — number of slots currently held.
- Semaphore.Cap — total capacity (maximum concurrent slots).
- Semaphore.Utilization — instantaneous utilization as a float64 in [0.0, 1.0], computed as Len / Cap.
- Semaphore.UtilizationSmoothed — exponentially weighted moving average of utilization, updated on each Release/ReleaseN call. Provides a smoothed view of usage over time for adaptive concurrency control or capacity planning.
- Semaphore.IsEmpty — true when no slots are held (Len == 0).
- Semaphore.IsFull — true when all slots are held (Len == Cap).
## Dynamic capacity
- Semaphore.SetCap — adjusts the semaphore's capacity at runtime. Pass -1 to reset to [defaultCap]. If the new capacity is smaller than the current occupancy, existing slots are drained. All blocked acquires are re-evaluated after a capacity change.
Acquire/release pairing ¶
Every successful acquire (Acquire, AcquireWith, TryAcquire, AcquireN, etc.) must be balanced by a corresponding release (Release or ReleaseN). The idiomatic pattern uses defer:
sem.Acquire() defer sem.Release() // ... guarded work ...
For bulk operations:
if err := sem.AcquireN(5); err != nil {
return err
}
defer sem.ReleaseN(5)
// ... batch work using 5 slots ...
Failing to release a slot permanently reduces the effective capacity of the semaphore. Releasing more slots than were acquired returns ErrReleaseExceedsCount.
Graceful shutdown pattern ¶
A typical shutdown sequence stops accepting new work, waits for in-flight operations to complete, then drains any stragglers:
// 1. Stop dispatching new work (application-specific).
close(stopCh)
// 2. Wait for in-flight work to release its slots.
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := sem.Wait(ctx); err != nil {
log.Printf("timed out waiting for drain: %v", err)
// 3. Force-drain remaining slots.
_ = sem.Drain()
}
Adaptive concurrency example ¶
The smoothed utilization metric can drive dynamic capacity adjustments:
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for range ticker.C {
u := sem.UtilizationSmoothed()
switch {
case u > 0.9:
_ = sem.SetCap(sem.Cap() * 2) // scale up
case u < 0.3 && sem.Cap() > minCap:
_ = sem.SetCap(sem.Cap() / 2) // scale down
}
}
}()
HTTP middleware example ¶
Semaphore works well as a request-level concurrency limiter:
var reqSem = sema.Must(100)
func LimitMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := reqSem.AcquireWith(r.Context()); err != nil {
http.Error(w, "service busy", http.StatusServiceUnavailable)
return
}
defer reqSem.Release()
next.ServeHTTP(w, r)
})
}
Worker pool example ¶
func processItems(ctx context.Context, items []Item) error {
sem := sema.Must(10)
var g errgroup.Group
for _, item := range items {
item := item
if err := sem.AcquireWith(ctx); err != nil {
return err
}
g.Go(func() error {
defer sem.Release()
return process(item)
})
}
return g.Wait()
}
func Must ¶ added in v1.1.0
Must is a convenience wrapper around New that panics instead of returning an error. It is intended for use in package-level variable declarations and program initialization where an invalid capacity is a programmer error that should be caught immediately.
Must panics with a message prefixed by "sema.Must:" if c is invalid. The same capacity rules as New apply: pass a positive integer for an explicit capacity, or -1 to use [defaultCap].
When to use Must vs New ¶
Use Must when the capacity is a compile-time constant or derived from a trusted configuration value that has already been validated. Use New when the capacity comes from user input, a dynamic configuration source, or any context where a graceful error is preferable to a panic.
Examples ¶
Package-level semaphore with a fixed capacity:
var dbPool = sema.Must(25)
func QueryDB(ctx context.Context, q string) (*Result, error) {
if err := dbPool.AcquireWith(ctx); err != nil {
return nil, fmt.Errorf("pool full: %w", err)
}
defer dbPool.Release()
return execQuery(q)
}
Using the default capacity:
var workers = sema.Must(-1)
Panics on invalid input (useful for catching bugs early):
var bad = sema.Must(0) // panics: "sema.Must: sema: capacity must be >= 1 or -1 for default, got 0"
func New ¶
New creates a new Semaphore with the given capacity c, which defines the maximum number of slots that may be held concurrently. The capacity can be changed later at runtime via [semaphore.SetCap].
The special value -1 may be passed for c to use the package-level [defaultCap]. Any other value less than 1 returns ErrInvalidCap.
The returned Semaphore starts empty (zero slots held) and is immediately ready for use. All methods on the returned value are safe for concurrent use from multiple goroutines.
Capacity semantics ¶
The capacity represents the upper bound on concurrent access. When all c slots are held, calls to [semaphore.Acquire] block and calls to [semaphore.TryAcquire] return false until a slot is freed by [semaphore.Release].
Observability ¶
The semaphore returned by New has no observer attached. Use NewWithObserver to attach an Observer that receives callbacks on acquire, release, and wait events.
Examples ¶
Creating a semaphore with an explicit capacity:
sem, err := sema.New(10)
if err != nil {
log.Fatalf("failed to create semaphore: %v", err)
}
sem.Acquire()
defer sem.Release()
Using the default capacity:
sem, err := sema.New(-1)
if err != nil {
log.Fatalf("failed to create semaphore: %v", err)
}
fmt.Printf("default capacity: %d\n", sem.Cap())
Handling invalid input:
sem, err := sema.New(0)
if err != nil {
// err is ErrInvalidCap{Value: 0}
var capErr sema.ErrInvalidCap
if errors.As(err, &capErr) {
fmt.Printf("invalid capacity: %d\n", capErr.Value)
}
}
func NewWithObserver ¶ added in v1.1.0
NewWithObserver creates a new Semaphore with the given capacity and attaches an Observer that receives lifecycle callbacks. It is otherwise identical to New — the same capacity rules, error conditions, and concurrency guarantees apply.
The observer is called synchronously under the semaphore's internal lock for acquire and release events, so observer implementations should be fast and non-blocking to avoid degrading throughput.
Observer callbacks ¶
The attached Observer receives the following notifications:
- Observer.OnAcquire(len, cap) — called after a slot is successfully acquired, with the current occupancy and capacity.
- Observer.OnRelease(len, cap) — called after a slot is released.
- Observer.OnWaitStart() — called when [semaphore.Wait] begins.
- Observer.OnWaitEnd(err) — called when [semaphore.Wait] finishes, with nil on success or the cancellation error.
Examples ¶
Attaching a metrics observer:
type metricsObserver struct{}
func (m metricsObserver) OnAcquire(len, cap int) {
metrics.Gauge("semaphore.utilization").Set(float64(len) / float64(cap))
}
func (m metricsObserver) OnRelease(len, cap int) {
metrics.Gauge("semaphore.utilization").Set(float64(len) / float64(cap))
}
func (m metricsObserver) OnWaitStart() {}
func (m metricsObserver) OnWaitEnd(err error) {}
sem, err := sema.NewWithObserver(50, metricsObserver{})
if err != nil {
log.Fatalf("failed to create semaphore: %v", err)
}
Attaching a logging observer for debugging:
type debugObserver struct{ logger *log.Logger }
func (d debugObserver) OnAcquire(len, cap int) {
d.logger.Printf("acquired: %d/%d slots in use", len, cap)
}
func (d debugObserver) OnRelease(len, cap int) {
d.logger.Printf("released: %d/%d slots in use", len, cap)
}
func (d debugObserver) OnWaitStart() { d.logger.Println("wait started") }
func (d debugObserver) OnWaitEnd(err error) { d.logger.Printf("wait ended: %v", err) }
sem, err := sema.NewWithObserver(5, debugObserver{logger: log.Default()})
if err != nil {
log.Fatalf("failed to create semaphore: %v", err)
}