A busy-waiting loop repeatedly checks whether work can proceed, even when nothing has changed. In concurrent Go code, the loop may produce the right result while wasting CPU cycles. The fix depends on why it keeps running: a listener may need to wait for a state change, or a select loop may need to stop considering a closed channel. section 9.12 section 9.6
type Donation struct { #1
mu sync.RWMutex
balance int
}
donation := &Donation{}
// Listener goroutines
f := func(goal int) { #2
donation.mu.RLock()
for donation.balance < goal { #3
donation.mu.RUnlock()
donation.mu.RLock()
}
fmt.Printf("$%d goal reached\n", donation.balance)
donation.mu.RUnlock()
}
go f(10)
go f(15)
// Updater goroutine
go func() {
for { #4
time.Sleep(time.Second)
donation.mu.Lock()
donation.balance++
donation.mu.Unlock()
}
}()
The donation listeners repeatedly release and reacquire a read lock while checking their goals. The updater changes the balance only once per second, but the listeners keep looping between updates. section 9.12
Sending each new balance on one shared channel does not solve the listener problem: each sent value reaches only one receiver. A listener waiting for $10 can miss that update and report its goal at $11 instead. When every listener needs a chance to check the changed balance, use a condition variable: listeners call Wait while their goal is unmet, and the updater calls Broadcast after changing the balance. section 9.12
type Donation struct {
cond *sync.Cond #1
balance int
}
donation := &Donation{
cond: sync.NewCond(&sync.Mutex{}), #2
}
// Listener goroutines
f := func(goal int) {
donation.cond.L.Lock()
for donation.balance < goal {
donation.cond.Wait() #3
}
fmt.Printf("%d$ goal reached\n", donation.balance)
donation.cond.L.Unlock()
}
go f(10)
go f(15)
// Updater goroutine
for {
time.Sleep(time.Second)
donation.cond.L.Lock()
donation.balance++ #4
donation.cond.L.Unlock()
donation.cond.Broadcast() #5
}
Each listener checks its goal under the condition variable’s lock and waits rather than polling. After a broadcast, it checks the goal again; the updater increments the balance under the lock. section 9.12
A closed channel that keeps a loop running
A merge loop receives from two channels using select. If one channel closes, recording that fact in a Boolean does not remove its receive case. That case remains ready, so the loop can repeatedly select it while waiting for the other channel. Assign the closed channel variable to nil instead; the next select waits only on the remaining channel. section 9.6
func merge(ch1, ch2 <-chan int) <-chan int {
ch := make(chan int, 1)
go func() {
for ch1 != nil || ch2 != nil { #1
select {
case v, open := <-ch1:
if !open {
ch1 = nil #2
break
}
ch <- v
case v, open := <-ch2:
if !open {
ch2 = nil #3
break
}
ch <- v
}
}
close(ch)
}()
return ch
}
The merge loop sets each input to nil when it closes. It exits and closes the output only after both inputs have been disabled. section 9.6


