Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Some links on this page are affiliate links: if you buy through them we may earn a commission, at no extra cost to you.

sync.Cond is Go’s condition-variable primitive. It lets goroutines sleep until shared state may have changed, then wake one another with Signal or Broadcast. The essential pattern is:

mu.Lock()
for !condition {
    cond.Wait()
}
useSharedState()
mu.Unlock()

The condition itself is ordinary application state—such as ready == true or len(queue) > 0. A mutex protects that state; sync.Cond coordinates goroutines waiting for it.

What problem does sync.Cond solve?

Concurrency has two separate problems:

  • Mutual exclusion: only one goroutine should access shared state at a time. Use sync.Mutex or sync.RWMutex.
  • Waiting for state: a goroutine should pause until a queue has an item, initialization is complete, or capacity is available. Use a condition variable or another coordination mechanism.

A mutex protects a condition, but it does not provide an efficient way to sleep until that condition changes. This busy-waiting loop wastes CPU:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
for !ready {
    // Repeated polling
}

sync.Cond allows the goroutine to sleep while another goroutine changes the shared state.

#1 Best Overall
Sale
C: A Reference Manual, 5th Edition
  • c
  • c programming
  • programming language
  • reference

What is a condition or predicate?

A Cond does not store or understand the condition. Your program owns the predicate and the data it examines. Examples include:

ready == true
len(queue) > 0
len(queue) < capacity
activeWorkers == 0
state == "closed"

The predicate’s state must be read and changed consistently while holding the associated lock. A notification means that the predicate may now be true; it does not guarantee that it is true when the awakened goroutine gets the lock.

Creating a condition variable

Create a condition variable with sync.NewCond and a locker, normally a pointer to a mutex:

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.
mu := &sync.Mutex{}
cond := sync.NewCond(mu)

The locker must implement:

type Locker interface {
    Lock()
    Unlock()
}

A *sync.RWMutex can also implement the interface, but condition-variable designs involving read locks are easier to misuse. For most code, use *sync.Mutex unless there is a clear reason not to.

Unlike a mutex, a Cond needs an associated locker supplied through NewCond. Prefer a constructor when a type owns one:

type Gate struct {
    mu    sync.Mutex
    cond  *sync.Cond
    ready bool
}

func NewGate() *Gate {
    g := &Gate{}
    g.cond = sync.NewCond(&g.mu)
    return g
}

Do not copy a Cond after it has been used. Prefer pointers to objects containing synchronization primitives.

See the official sync package documentation and sync.Cond source documentation for the API contract.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How Wait works

A caller must hold cond.L before calling Wait. Wait then:

  1. Registers the goroutine as a waiter.
  2. Atomically unlocks the associated locker.
  3. Suspends the goroutine.
  4. Resumes after Signal or Broadcast.
  5. Reacquires the locker before returning.

That atomic unlock-and-wait step prevents a notification from being lost between checking the predicate and going to sleep.

mu.Lock()
for !condition() {
    cond.Wait()
}
// The lock is held again here.
useSharedState()
mu.Unlock()

The lock is not held while the goroutine sleeps, but it is held again when Wait returns.

Why Wait must be inside a for loop

Use this:

mu.Lock()
for len(queue) == 0 {
    cond.Wait()
}
item := queue[0]
queue = queue[1:]
mu.Unlock()

Do not use a single if:

mu.Lock()
if len(queue) == 0 {
    cond.Wait()
}
item := queue[0] // The queue may be empty again.
mu.Unlock()

Suppose several consumers are waiting and a producer adds one item, then calls Broadcast. All consumers wake, but only one can acquire the mutex first and remove the item. The others must wait for the lock, then recheck the predicate and go back to sleep.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Go documents that Wait does not return unless awakened by Signal or Broadcast. Nevertheless, a loop is mandatory because waking means “the state may have changed,” not “the condition is reserved for this goroutine.”

Signal versus Broadcast

Method Effect Typical use
Signal Wakes at most one waiter. One newly added queue item can satisfy one consumer.
Broadcast Wakes all current waiters. Shutdown, readiness transitions, or state changes that may help many waiters.

Signal does not promise FIFO order, fairness, or scheduling priority. Code must not depend on which waiter runs first.

The API permits calling either method with or without holding cond.L. In practice, holding the lock while changing the predicate and notifying is usually easier to reason about:

mu.Lock()
ready = true
cond.Broadcast()
mu.Unlock()

The lock is still required when safely reading or changing the predicate, even though it is not formally required just to call Signal or Broadcast.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example: wait until initialization is ready

package main

import (
    "fmt"
    "sync"
)

type Starter struct {
    mu    sync.Mutex
    cond  *sync.Cond
    ready bool
}

func NewStarter() *Starter {
    s := &Starter{}
    s.cond = sync.NewCond(&s.mu)
    return s
}

func (s *Starter) WaitUntilReady() {
    s.mu.Lock()
    defer s.mu.Unlock()

    for !s.ready {
        s.cond.Wait()
    }
}

func (s *Starter) SetReady() {
    s.mu.Lock()
    defer s.mu.Unlock()

    s.ready = true
    s.cond.Broadcast()
}

func main() {
    starter := NewStarter()

    var wg sync.WaitGroup
    wg.Add(1)
    go func() {
        defer wg.Done()
        starter.WaitUntilReady()
        fmt.Println("worker: starting")
    }()

    // Initialization would happen here in a real program.
    starter.SetReady()
    wg.Wait()
}

The ready field is protected by mu. The waiter releases the mutex while sleeping, and SetReady updates the state and wakes all current waiters. A future waiter simply observes ready == true and does not wait.

Notifications are not queued events

A condition variable is not a message queue. This is not a durable event:

cond.Signal()

If nobody is waiting at that moment, there may be nobody to wake. Store durable information in shared state instead:

mu.Lock()
ready = true
cond.Broadcast()
mu.Unlock()

A later waiter sees ready == true and proceeds. If the program needs to retain messages or events, a channel or explicit queue is usually the better abstraction.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Example: a bounded producer–consumer queue

A bounded queue has two useful predicates:

  • notEmpty: consumers can proceed when len(items) > 0.
  • notFull: producers can proceed when len(items) < capacity.
package queue

import (
    "errors"
    "sync"
)

var ErrClosed = errors.New("queue is closed")

type Queue[T any] struct {
    mu       sync.Mutex
    notEmpty *sync.Cond
    notFull  *sync.Cond

    items  []T
    cap    int
    closed bool
}

func NewQueue[T any](capacity int) *Queue[T] {
    if capacity <= 0 {
        panic("capacity must be positive")
    }

    q := &Queue[T]{cap: capacity}
    q.notEmpty = sync.NewCond(&q.mu)
    q.notFull = sync.NewCond(&q.mu)
    return q
}

func (q *Queue[T]) Put(item T) error {
    q.mu.Lock()
    defer q.mu.Unlock()

    for len(q.items) == q.cap && !q.closed {
        q.notFull.Wait()
    }

    if q.closed {
        return ErrClosed
    }

    q.items = append(q.items, item)
    q.notEmpty.Signal()
    return nil
}

func (q *Queue[T]) Get() (T, error) {
    q.mu.Lock()
    defer q.mu.Unlock()

    for len(q.items) == 0 && !q.closed {
        q.notEmpty.Wait()
    }

    if len(q.items) == 0 && q.closed {
        var zero T
        return zero, ErrClosed
    }

    item := q.items[0]
    q.items[0] = *new(T)
    q.items = q.items[1:]
    q.notFull.Signal()
    return item, nil
}

func (q *Queue[T]) Close() {
    q.mu.Lock()
    defer q.mu.Unlock()

    if q.closed {
        return
    }

    q.closed = true
    q.notEmpty.Broadcast()
    q.notFull.Broadcast()
}

Adding one item signals one consumer. Removing one item signals one producer. Closing broadcasts to both groups because blocked producers and consumers must be given a chance to observe closed and exit.

This example drains already-buffered items after closure: consumers receive those items before returning ErrClosed. A production queue may instead choose different policies, such as cancellation with a context, rejecting all operations immediately, or returning a separate shutdown error.

Common mistakes

Calling Wait without the lock

cond.Wait() // Incorrect: cond.L is not held.

The caller must hold the associated locker when calling Wait.

Using if instead of for

Always recheck the predicate after waking. Another goroutine may have consumed the resource first.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Reading the predicate outside the lock

if ready { ... } // A race if another goroutine writes ready.

Protect every relevant read and write with the same mutex, or use a properly designed atomic operation when a mutex is not appropriate.

Signaling without changing durable state

A notification should normally accompany a state transition. Otherwise, a waiter may wake, find the predicate false, and sleep again without progress.

Forgetting shutdown

This waiter can remain blocked forever if the queue closes while empty:

for len(queue) == 0 {
    cond.Wait()
}

Include shutdown in the predicate:

for len(queue) == 0 && !closed {
    cond.Wait()
}

Holding the mutex during slow work

Claim or copy the needed state, then unlock before expensive or blocking work. Otherwise, other goroutines may be unable to change the predicate or wake waiters.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Expecting fairness

Signal wakes one waiter but does not guarantee which waiter runs. Do not build correctness around FIFO behavior.

Copying a used Cond

Do not pass a used Cond by value or copy a struct containing one after coordination has begun. Use pointers and constructors.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Memory visibility and happens-before

The producer should update shared state under the mutex, and the consumer should inspect it under the same mutex. This gives the program a synchronized lock boundary instead of unsafely sharing ordinary variables between goroutines.

Go’s sync.Cond documentation specifies that a Signal or Broadcast synchronizes before the Wait call it unblocks. The condition variable provides waiting and notification; it does not replace the mutex that protects the state. See the Go memory model for the broader rules.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Cancellation and timeouts

sync.Cond has no built-in timeout or context-aware Wait. Cancellation must be represented in the predicate and followed by a notification:

for !ready && !cancelled {
    cond.Wait()
}

The cancellation path must set cancelled under the mutex and call Broadcast so blocked goroutines can observe it. If deadlines, cancellation, or select are central to the design, channels and context.Context are often easier to compose.

sync.Cond versus channels and other tools

Need Good default
Transfer work or results Channel
Wait for permanent readiness Closed channel or sync.Once, depending on the lifecycle
Manage a shared bounded queue sync.Cond or a channel-based queue
Wake all waiters during shutdown Broadcast or close a channel
Timeouts, cancellation, or select Channel plus context.Context
One numeric flag or counter sync/atomic, if the design truly needs atomic access
Limit concurrent work Buffered channel semaphore or a suitable semaphore abstraction

Use sync.Cond when several goroutines wait on persistent shared state protected by a mutex, especially when there are multiple predicates such as “not empty” and “not full.” Use channels when the central operation is communication or value transfer. Go’s documentation explicitly notes that channels are preferable for many simple use cases, but they are not a universal replacement for condition variables.

Do not assume that sync.Cond is always faster than channels. The right choice depends on semantics, ownership, cancellation requirements, clarity, and the workload.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Testing a condition-variable design

Create a temporary module if you want to experiment:

mkdir cond-demo
cd cond-demo
go mod init example.com/cond-demo

Run tests with the race detector:

go test
go test -race
go run -race .

Tests should cover:

  • A worker remains blocked while readiness is false.
  • Setting readiness allows the worker to continue.
  • Several waiters all continue after Broadcast.
  • A consumer cannot remove an item from an empty queue.
  • Close wakes blocked producers and consumers.
  • Repeated producer–consumer runs do not deadlock.

Avoid asserting exact goroutine scheduling or assuming that Signal selects a particular waiter. The race detector finds races only on executed paths, so meaningful tests must exercise waiting, signaling, broadcasting, and shutdown. More details are available in Go’s race detector documentation.

A practical checklist

  • Is the predicate ordinary shared state protected by the associated lock?
  • Is every Wait call inside a for loop?
  • Is the lock held when observing or changing the predicate?
  • Does each relevant state transition notify the right waiters?
  • Does the predicate include shutdown or cancellation?
  • Is Signal used for one available unit and Broadcast for global transitions?
  • Does the code avoid relying on fairness or FIFO ordering?
  • Is the Cond never copied after first use?
  • Have tests been run with go test -race?

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.