sync.Cond.Wait resumes after a notification, but the notification does not assert that a caller-specific condition is still true when the goroutine reacquires the lock. The shared predicate remains the source of truth, so a waiter checks it again after every return from Wait.
This boundary separates notification from state. Signal and Broadcast announce that relevant state may have changed; they do not transfer ownership of that state or reserve it for a particular waiter.
Wait unlocks and later reacquires the locker
A Cond is associated with a Locker, commonly a *sync.Mutex. A caller holds that lock while inspecting shared state. If the predicate is false, Wait atomically unlocks the locker and suspends the caller. Before Wait returns, it locks the locker again.
mu.Lock()
for len(queue) == 0 {
cond.Wait()
}
item := queue[0]
queue = queue[1:]
mu.Unlock()The loop is part of the synchronization contract around the shared predicate. After wakeup and lock reacquisition, another goroutine may already have changed the state that caused the notification.
Notification does not reserve shared state
Suppose several goroutines wait for a non-empty queue. A producer appends one item and signals a waiter. The selected waiter becomes runnable, but it does not execute outside normal scheduler and mutex competition.
Before that waiter reacquires the mutex, another goroutine can acquire the mutex and consume the item. When the signaled waiter eventually returns from Wait, the queue can be empty again.
The signal remains valid as an event: the queue did change. It is not a durable claim on the item. Treating notification as reservation can produce invalid reads, stale assumptions, or operations against state that no longer satisfies the required predicate.
Signal selects one waiter without defining application ownership
Signal wakes one goroutine waiting on the condition, if one exists. The package does not turn that selection into ownership of application data.
The awakened goroutine still has to reacquire the associated lock. Lock acquisition order is not an application-level handoff protocol, and other goroutines competing for the same mutex can run first.
For state representing a limited resource, the protected predicate decides whether the resource remains available. A waiter that sees the predicate become false again returns to Wait rather than proceeding on the earlier notification.
Broadcast creates contention around one state transition
Broadcast wakes all current waiters. This is appropriate when one state transition can make progress possible for multiple blocked goroutines, or when each waiter has a distinct predicate over the same protected state.
All awakened goroutines still serialize through the locker before inspecting state. If only one can consume the newly available resource, the others can reacquire the lock, observe a false predicate, and wait again.
A broadcast can therefore create a burst of runnable goroutines without guaranteeing equal progress. Its cost includes scheduler activity and lock contention proportional to the waiting population, so it is materially different from signaling one waiter.
Predicate mutation and notification belong to one synchronization design
The shared state is normally changed while holding the same lock used by waiters. This gives predicate inspection and mutation a consistent synchronization boundary.
mu.Lock()
queue = append(queue, item)
cond.Signal()
mu.Unlock()The notification may occur while the lock is held. The awakened waiter cannot return from Wait until it reacquires that lock, so it observes state through the mutex synchronization edge rather than through the notification alone.
Calling Signal or Broadcast does not itself require holding the lock, but separating state mutation from the locking discipline can make the protocol fragile. The critical property is that waiters cannot miss the state transition between checking the predicate and entering the wait state; Wait provides the atomic unlock-and-suspend operation needed for that boundary.
Cond carries no event history
A condition variable does not retain notifications for future waiters. If Signal runs when no goroutine is waiting, no token is stored for a later Wait call.
That behavior differs from a buffered channel or a semaphore-like counter. A future waiter must inspect current shared state rather than infer anything from earlier notifications.
This also makes predicate checks before Wait essential. If the desired state is already present, the goroutine proceeds directly. Waiting first would depend on a future notification that may never arrive even though the state already permits progress.
The predicate defines correctness
A Cond coordinates goroutines around state guarded elsewhere. The condition variable does not encode queue length, capacity, lifecycle phase, generation number, or any other application invariant.
A useful predicate is therefore explicit and testable under the associated lock. Examples include len(queue) > 0, active < limit, or generation != observedGeneration. Multiple predicates can share one condition variable when the same state changes are relevant to each, though broader broadcasts can increase unnecessary wakeups.
The durable contract resides in the protected state. Wait provides an atomic transition from locked inspection to suspension and back to locked inspection; Signal and Broadcast provide wakeup hints tied to state changes. Rechecking the predicate connects those mechanisms without treating transient notifications as persistent state.