How do you create a sync.Cond in Go, and what does its Wait method do to c.L?
answer
- it needs a lock to work with
- you must already hold it
- released while asleep, held again on return
- sync.NewCond takes a sync.Locker
- the state may have moved while you slept
basics
~20 ssync.NewCond(l) builds a *sync.Cond over any sync.Locker, stored in its L field. Wait must be called with c.L already held: it atomically unlocks c.L and parks the goroutine, then re-locks c.L before returning to the caller.
solid answer
~40 sA `*sync.Cond` is built with `sync.NewCond(l)`, where `l` is any `sync.Locker` — usually the `*sync.Mutex` that guards the shared state the condition is about — and it is reachable afterwards as the `L` field. The contract is that you lock `c.L`, inspect the state, and if it is not what you need you call `c.Wait()`. `Wait` atomically releases `c.L` and suspends the goroutine, so other goroutines can take the lock and change the state; when `Signal` or `Broadcast` wakes it, `Wait` re-acquires `c.L` before it returns, so the caller is holding the lock again on the line after the call. Because the lock was released in between, whatever you checked before waiting may no longer be true, so you re-check the state after `Wait` returns rather than assuming it.
code
go · 22 linestype queue struct {
mu sync.Mutex
notEmpty *sync.Cond
items []string
}
func newQueue() *queue {
q := &queue{}
q.notEmpty = sync.NewCond(&q.mu)
return q
}
func (q *queue) take() string {
q.mu.Lock()
defer q.mu.Unlock()
for len(q.items) == 0 {
q.notEmpty.Wait() // q.mu is released while parked, held again on return
}
v := q.items[0]
q.items = q.items[1:]
return v
}go deeper
Be ready to say that a sync.Cond is built with sync.NewCond over the mutex guarding the data, that Wait must be called with that lock already held, and that Wait gives the lock up while asleep and takes it back before returning.
An interviewer expects you to explain why releasing the lock and parking must be one atomic step, and what the released window means for anything the waiter checked beforehand.
Show you can read a goroutine dump, recognise waiters sitting in sync.(*Cond).Wait, and name exactly which code path in your service is responsible for waking them.
Own the decision of whether a package you ship exposes a Cond-backed blocking call at all, given that callers cannot put a deadline on it, cannot select on it, and cannot see it in their own cancellation tree.
## What `sync.Cond` is `sync.Cond` is Go's condition variable: a place for goroutines to park until some shared, lock-protected state changes. It does not carry data and it does not do any locking on its own — it is always paired with a lock that protects the state the goroutines care about. The whole type is three methods and one field: - `func sync.NewCond(l sync.Locker) *sync.Cond` - `func (c *sync.Cond) Wait()` - `func (c *sync.Cond) Signal()` - `func (c *sync.Cond) Broadcast()` - the field `c.L sync.Locker` `sync.Locker` is just the interface `{ Lock(); Unlock() }`, so a `*sync.Mutex` qualifies, and so does a `*sync.RWMutex` (or the read-only `Locker` its `RLocker` method returns). In practice you pass the mutex that already guards the data — that pairing is the point. ## Construction Unlike `sync.Mutex`, the zero `sync.Cond` is **not** ready to use, because its `L` field would be a nil `Locker` and the first `Wait` would dereference nil. Build it with `sync.NewCond`, typically right after the struct that owns it, and store the pointer: ``` q := &queue{} q.notEmpty = sync.NewCond(&q.mu) ``` Always pass and store `*sync.Cond`, never a `sync.Cond` value: the type contains a copy checker, and using a copy made after first use panics with `sync.Cond is copied`. ## The `Wait` contract, which is the part interviews test Three things happen in `Wait`, in this order: 1. It **unlocks `c.L`**. This is why you must already hold `c.L` when you call it — `Wait` starts by calling `c.L.Unlock()`. Calling it without the lock held is not a friendly error: with a `*sync.Mutex` you get `fatal error: sync: unlock of unlocked mutex`, which is a runtime fatal error that `recover` cannot catch and that takes the whole process down. 2. It **parks the goroutine** on the condition's internal notify list. The release and the park are atomic with respect to `Signal`/`Broadcast`, so a wake-up that happens "just after" you released the lock is not lost. 3. When woken, it **re-acquires `c.L`** and only then returns. So on the statement after `c.Wait()` you are holding the lock again, exactly as you were before the call, and a `defer c.L.Unlock()` at the top of the function is still correct. The important consequence is the middle step: while you are parked, you hold nothing. Other goroutines take the lock and mutate the state freely. So the fact that you were woken tells you only that somebody called `Signal` or `Broadcast` — it does not tell you that the thing you are waiting for is still true when you get the lock back. Another goroutine may have woken first and consumed the item. That is why the shared state, not the wake-up, is the source of truth: you re-read it after `Wait` returns. Go's documentation adds a guarantee that not every condition-variable implementation makes: `Wait` cannot return unless it was woken by `Broadcast` or `Signal`. There are no spurious wakeups in Go. The re-check is still needed, but for the lock-gap reason above rather than for random wakeups. ## A worked shape An embedded message broker holding a bounded in-memory queue is the classic fit: consumers block until there is something to take. ``` func (q *queue) take() string { q.mu.Lock() defer q.mu.Unlock() for len(q.items) == 0 { q.notEmpty.Wait() } v := q.items[0] q.items = q.items[1:] return v } ``` Every mutation of `q.items` elsewhere happens under `q.mu` and is followed by a `Signal` or `Broadcast` on `q.notEmpty`. The consumer never touches the condition variable except through this shape. ## What goes wrong - Calling `Wait` without holding `c.L` — fatal error, process dies. - A `sync.Cond` declared as a value and copied into a method receiver or a channel — `sync.Cond is copied` panic. - Building a `Cond` over one lock while guarding the state with a different one — the release-and-repark window is then unprotected and the state can change without any waiter being woken. - Treating `Wait` as a sleep. It is not timed; nothing but `Signal` or `Broadcast` on that same `Cond` will bring the goroutine back, which is why a parked waiter shows up in a goroutine dump sitting in `sync.(*Cond).Wait` and stays there.
- What happens if a goroutine calls Wait on a sync.Cond without holding c.L first?`Wait`'s first action is `c.L.Unlock()`. With a `*sync.Mutex` that is an unlock of an unlocked mutex, which is `fatal error: sync: unlock of unlocked mutex` — a runtime fatal error, not a panic, so `recover` cannot catch it and the whole process dies. There is no lazy-locking convenience here; the lock must already be held.
- Can Wait return spuriously in Go, without any Signal or Broadcast?No. The `sync` documentation states that `Wait` cannot return unless woken by `Broadcast` or `Signal`, so Go has no spurious wakeups. You still re-read the shared state after `Wait` returns, because `c.L` was released the whole time you were parked and another goroutine may have consumed what you were waiting for.
- Does sync.NewCond require a *sync.Mutex specifically?No — it takes a `sync.Locker`, the `{ Lock(); Unlock() }` interface. A `*sync.Mutex` is the usual choice, but a `*sync.RWMutex` works, and so does the `sync.Locker` returned by `RWMutex.RLocker()`. Whatever you pass must be the same lock that guards the state the waiters inspect, or the wake-up window is unprotected.
- Is it safe to copy a sync.Cond value?No. `sync.Cond` carries a copy checker, and calling `Wait`, `Signal` or `Broadcast` on a copy made after first use panics with `sync.Cond is copied`. Always hold and pass the `*sync.Cond` that `sync.NewCond` returned — store the pointer in the struct, and give methods a pointer receiver so the struct is never copied.
It is like stepping out of a meeting room and leaving the door unlocked behind you: while you are outside anyone can go in and rearrange things, and when you are called back you must take the room again and look at what is actually on the table now.
saying these in an interview costs you the question
- Says Wait keeps c.L held while the goroutine is parked
- Thinks Wait locks c.L for you if you forgot
- Assumes the awaited state is guaranteed true when Wait returns
- Uses a zero-value sync.Cond without setting L via NewCond
- Copies the sync.Cond into a value receiver or another struct
- Calls Wait as a way to sleep for a while