Implement ping-pong between two goroutines that stops cleanly after N rounds or on context cancellation.
Pass a "ball" (a hit counter) between two players. Each player selects on both its input channel and ctx.Done(). The same applies to the send: a send that is not inside a select can block forever if the other player has already quit. The player that sees the last round calls cancel(), so its partner wakes up and exits too.
func PingPong(ctx context.Context, rounds int) {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
ping, pong := make(chan int), make(chan int)
var wg sync.WaitGroup
player := func(name string, in <-chan int, out chan<- int) {
defer wg.Done()
for {
select {
case <-ctx.Done():
return
case ball := <-in:
if ball >= rounds {
cancel() // wake the partner
return
}
fmt.Println(name, ball)
select {
case out <- ball + 1:
case <-ctx.Done():
return
}
}
}
}
wg.Add(2)
go player("ping", ping, pong)
go player("pong", pong, ping)
select { // serve
case ping <- 0:
case <-ctx.Done():
}
wg.Wait()
}
What does this print?
func main() {
ch := make(chan int)
go func() { v := <-ch; ch <- v + 1 }()
ch <- 1
ch <- 2
fmt.Println(<-ch)
}
Answer: fatal error: all goroutines are asleep - deadlock!. The goroutine receives 1 and then blocks sending 2 back. Main is blocked sending 2 at the same moment. Both goroutines are trying to send on the same unbuffered channel and nobody is receiving. This is why ping-pong uses two directional channels. Be aware that the runtime only reports a deadlock when every goroutine is blocked. In a real server a stuck pair of goroutines just leaks silently.
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.
- Q518Print "foo", "bar", "baz" in strict order from three goroutines, repeated N times.
- 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.