# When to Use the Broadcast Package Over Go's sync.Cond: A Complete Guide

> Learn when to use the broadcast package over Go's sync.Cond for context-aware cancellation, select-able waiting, or zero value primitives. Get simple condition variable semantics with sync.Cond.

- Repository: [Aperture Robotics/util](https://github.com/aperturerobotics/util)
- Tags: deep-dive
- Published: 2026-02-25

---

**Use the **broadcast** package when you need context-aware cancellation, channel-based waiting with `select`, or a zero-value synchronization primitive; stick with **sync.Cond** for simple condition-variable semantics without cancellation requirements.**

The `aperturerobotics/util` repository provides a lightweight synchronization primitive in its `broadcast` package that solves specific pain points when working with Go's concurrency patterns. While Go's standard library `sync.Cond` has served as the traditional condition variable for goroutine coordination, the `broadcast` package offers distinct advantages for modern Go development that relies heavily on `context.Context` and the `select` statement.

## What Is the Broadcast Package?

The **broadcast** package, implemented in [`broadcast/broadcast.go`](https://github.com/aperturerobotics/util/blob/main/broadcast/broadcast.go), provides a `Broadcast` type built on top of a `sync.Mutex`. Unlike `sync.Cond`, which requires explicit initialization with a mutex, the zero value of `Broadcast` is immediately ready for use. The package implements a channel-centric notification system where waiters receive signals via closed channels rather than through the opaque `Wait()` method of `sync.Cond`.

## Key Differences: Broadcast Package vs sync.Cond

### Context Cancellation Support

The `broadcast` package natively supports `context.Context` cancellation through its `Wait` method. When you call `Wait(ctx, predicate)`, the method returns `context.Canceled` immediately if the context expires or is cancelled, without requiring additional goroutine management.

In contrast, `sync.Cond` has no built-in cancellation mechanism. You must implement cancellation manually by combining `cond.Wait()` with a separate channel and `select` statement, which increases code complexity and risks subtle race conditions.

### Integration with Go's select Statement

The `broadcast` package provides the `getWaitCh` function (accessible within `HoldLock` callbacks) that returns a receive-only channel which closes when a broadcast occurs. This design allows natural integration with Go's `select` statement, enabling you to wait for either a broadcast event or other channel operations simultaneously.

`sync.Cond` does not expose a channel interface. The `cond.Wait()` method blocks the calling goroutine without providing a channel to select on, making it impossible to combine with other event sources in a single `select` block.

### Zero-Value Convenience

The `Broadcast` type is designed to work immediately upon declaration without initialization. The zero value contains a valid internal mutex state, allowing you to use `var b broadcast.Broadcast` directly.

`sync.Cond` requires explicit initialization via `sync.NewCond(&mutex)` and will panic if used with a nil locker. This creates a potential source of runtime errors if initialization is forgotten or occurs in the wrong order.

### Notify-Once Semantics

The `broadcast` package implements "notify-once" semantics through the `broadcastLocked` function, which closes the current wait channel and clears it, while `getWaitChLocked` lazily creates a fresh channel for the next round of waiters. This pattern ensures that new waiters receive a distinct channel for each broadcast cycle.

`sync.Cond` does not track whether a notification has already occurred. Waiters that call `Wait()` after a `Signal` or `Broadcast` will block indefinitely unless the condition is re-checked and re-broadcast, requiring manual state management with an additional boolean flag.

## When to Choose the Broadcast Package

Prefer the `broadcast` package over `sync.Cond` when:

- You need **context-aware cancellation** with timeout or deadline support.
- You want to **select** between broadcast events and other channel operations.
- You prefer **zero-value initialization** without explicit constructor calls.
- You require **notify-once semantics** where each broadcast creates a fresh signal channel.
- You are building **complex coordination logic** that mixes multiple synchronization primitives.

## Code Examples

### Using broadcast.Broadcast for Context-Aware Waiting

The following example demonstrates the `HoldLock` and `Wait` methods from [`broadcast/broadcast.go`](https://github.com/aperturerobotics/util/blob/main/broadcast/broadcast.go), showing how to coordinate state updates with context cancellation:

```go
package main

import (
    "context"
    "fmt"
    "time"
    
    "github.com/aperturerobotics/util/broadcast"
)

func main() {
    var b broadcast.Broadcast
    var currentValue int
    
    // Producer goroutine
    go func() {
        for i := 0; i < 10; i++ {
            time.Sleep(20 * time.Millisecond)
            
            // HoldLock locks the mutex, executes the callback, and broadcasts
            b.HoldLock(func(broadcast func(), getWaitCh func() <-chan struct{}) {
                currentValue = i
                broadcast() // Closes the current wait channel
            })
        }
    }()
    
    // Consumer with timeout
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()
    
    err := b.Wait(ctx, func(broadcast func(), getWaitCh func() <-chan struct{}) (bool, error) {
        return currentValue == 9, nil
    })
    
    if err != nil {
        fmt.Printf("Wait failed: %v\n", err)
        return
    }
    
    fmt.Printf("Final value: %d\n", currentValue) // Output: Final value: 9
}

```

### Equivalent sync.Cond Implementation

To achieve similar functionality with `sync.Cond`, you must manually manage cancellation and channel integration:

```go
package main

import (
    "context"
    "fmt"
    "sync"
    "time"
)

func main() {
    var mu sync.Mutex
    cond := sync.NewCond(&mu)
    currentValue := 0
    done := make(chan struct{})
    
    // Producer
    go func() {
        for i := 0; i < 10; i++ {
            time.Sleep(20 * time.Millisecond)
            mu.Lock()
            currentValue = i
            cond.Broadcast()
            mu.Unlock()
        }
    }()
    
    // Consumer with manual cancellation
    ctx, cancel := context.WithTimeout(context.Background(), 500*time.Millisecond)
    defer cancel()
    
    go func() {
        mu.Lock()
        for currentValue != 9 {
            cond.Wait() // Blocks without context support
        }
        mu.Unlock()
        close(done)
    }()
    
    select {
    case <-done:
        fmt.Printf("Final value: %d\n", currentValue)
    case <-ctx.Done():
        fmt.Printf("Timeout: %v\n", ctx.Err())
    }
}

```

Notice the additional complexity required to handle context cancellation with `sync.Cond`, including the extra goroutine and channel management that the `broadcast` package handles internally.

## Implementation Details

The `broadcast` package in [`broadcast/broadcast.go`](https://github.com/aperturerobotics/util/blob/main/broadcast/broadcast.go) implements several key mechanisms:

- **`HoldLock`** and **`TryHoldLock`**: These methods acquire the internal `sync.Mutex`, execute a user-provided callback, and guarantee that broadcasting occurs while the lock is held. This pattern ensures that state changes and notifications are atomic.

- **`Wait`**: This method accepts a `context.Context` and a predicate function. It repeatedly evaluates the predicate under the lock; if the predicate returns false, it blocks on the channel returned by `getWaitCh` until a broadcast occurs or the context is cancelled.

- **`broadcastLocked`** and **`getWaitChLocked`**: These internal functions manage the channel lifecycle. `broadcastLocked` closes the current wait channel and clears it, while `getWaitChLocked` lazily creates a new channel for subsequent waiters, implementing the notify-once semantics.

## Summary

- Use the **broadcast** package over `sync.Cond` when you need **context cancellation**, **channel-based waiting** with `select`, or **zero-value initialization**.
- The `broadcast.Broadcast` type provides **notify-once semantics** automatically, while `sync.Cond` requires manual state tracking.
- Both primitives require holding a lock during broadcast, but `broadcast` encapsulates this pattern in `HoldLock` and `TryHoldLock`.
- For simple condition variables without cancellation needs, `sync.Cond` remains a valid standard library choice.

## Frequently Asked Questions

### Can I use the broadcast package with multiple goroutines waiting simultaneously?

Yes. The `broadcast.Broadcast` type is designed for concurrent use. When `broadcast()` is called within `HoldLock`, it closes the current wait channel, which unblocks all goroutines currently waiting on that channel via `Wait` or `getWaitCh`. The next call to `getWaitCh` creates a fresh channel for new waiters, ensuring that subsequent broadcasts only notify waiters that started waiting before the broadcast occurred.

### Does the broadcast package replace sync.Cond entirely?

No. While the `broadcast` package solves specific pain points like context cancellation and `select` integration, `sync.Cond` remains appropriate for simple condition-variable patterns where you don't need cancellation or channel-based waiting. If your code already manages cancellation externally and doesn't require `select` integration, `sync.Cond` avoids the slight overhead of channel creation and closure that `broadcast` uses to implement its semantics.

### How does context cancellation work in broadcast.Wait?

The `Wait` method in [`broadcast/broadcast.go`](https://github.com/aperturerobotics/util/blob/main/broadcast/broadcast.go) accepts a `context.Context` as its first argument. It enters a loop that first checks the user-provided predicate under the internal mutex. If the predicate returns false, it retrieves the current wait channel via `getWaitCh` and blocks on a `select` statement that waits for either the wait channel to close (indicating a broadcast) or the context to be done. If the context is cancelled or times out, `Wait` returns the context error immediately, allowing the caller to handle cancellation without additional goroutine coordination.

### Is the broadcast package safe for concurrent use?

Yes. The `Broadcast` struct embeds a `sync.Mutex` and all public methods (`HoldLock`, `TryHoldLock`, `Wait`) properly acquire this mutex before accessing internal state. The channel management logic in `broadcastLocked` and `getWaitChLocked` is always called while holding the lock, ensuring that channel creation, closure, and clearing happen atomically with respect to waiters. This makes the package safe for use across multiple goroutines without additional external synchronization.