Print "foo", "bar", "baz" in strict order from three goroutines, repeated N times.
Build a ring of channels. Goroutine i waits on chs[i], prints, and then signals chs[(i+1)%3]. Main starts it by putting a token into chs[0]. This generalizes to any number of workers.
func FooBarBaz(n int) {
words := []string{"foo", "bar", "baz"}
chs := make([]chan struct{}, len(words))
for i := range chs {
chs[i] = make(chan struct{}, 1) // buffer of 1: see gotcha
}
var wg sync.WaitGroup
for i, w := range words {
wg.Add(1)
go func() { // Go 1.22+: i and w are per-iteration, safe to capture
defer wg.Done()
next := chs[(i+1)%len(chs)]
for range n {
<-chs[i]
fmt.Println(w)
next <- struct{}{}
}
}()
}
chs[0] <- struct{}{}
wg.Wait()
}
Gotcha: on the last round "baz" sends a token into chs[0], but "foo" has already exited. With unbuffered channels that final send blocks forever and wg.Wait() deadlocks. You can fix it with a buffer of 1, as above, or by skipping the send on the last iteration. The leftover token is simply garbage collected.
Alternative: use one sync.Mutex + sync.Cond with a shared turn counter. Each goroutine waits for turn%3 != id, prints, increments, and calls Broadcast. Here you must use Broadcast, not Signal, because Signal might wake the wrong goroutine, which goes back to sleep, and then nobody makes progress.
What the interviewer is looking for: knowing that the scheduler guarantees nothing about order, so ordering has to be enforced explicitly (a time.Sleep "fix" fails immediately). Also that you handle the termination edge case and, before Go 1.22, the loop-variable capture bug.
More on Classic Concurrency Coding Problems
- Q516Print odd and even numbers alternately from two goroutines (1..N) using channels. Then do the same with sync.Mutex and sync.Cond.
- Q517Implement ping-pong between two goroutines that stops cleanly after N rounds or on context cancellation.
- Q519Implement the dining philosophers problem in Go without deadlock or starvation. Compare resource ordering with an arbiter.
- Q520Bank transfer: two goroutines transfer money between accounts A->B and B->A with per-account mutexes. Why does it deadlock, and how does consistent lock ordering fix it?
- Q521Implement a bounded blocking queue (producer-consumer) using sync.Cond, then using channels. Compare the two.
- Q522Implement a reusable barrier (CyclicBarrier) that N goroutines wait on before moving to the next phase.