Diagnosing a Deadlock
Ask someone to describe a Go deadlock and you'll usually hear about two goroutines each holding a lock the other wants — the classic lock-ordering problem. That bug is real, but it's also timing-dependent: whether it actually deadlocks on a given run can depend on which goroutine happens to acquire its first lock first, which makes it a bad fit for a graded exercise that has to fail the same way every time. The far more common deadlock in everyday Go code doesn't need two locks or bad luck at all — it needs one channel and a loop that expects one more value than will ever arrive.
func Collect(n int, work func(int) int) []int {
results := make(chan int)
for i := 0; i < n; i++ {
go func(i int) { results <- work(i) }(i)
}
out := make([]int, 0, n)
for i := 0; i <= n; i++ { // BROKEN: off-by-one — waits for n+1 results
out = append(out, <-results)
}
return out
}
n workers are started, and n values will ever be sent on results. The
collection loop's condition is i <= n, which runs n + 1 times — one
receive too many. The first n receives complete normally, in whatever
order the workers finish. The last one has no sender left. <-results
blocks, and nothing is ever going to unblock it: every goroutine that could
have sent has already sent and exited. This deadlocks every time,
regardless of scheduling, because it isn't a race about which goroutine
runs first — it's an arithmetic mistake that happens to manifest as a
channel operation with nothing on the other end.
Reading the failure
Run this under go test and you won't get a normal failure message. After
the test's timeout, Go prints every goroutine's stack trace and exits — a
"goroutine dump." Look for the goroutine blocked in your own code, not the
test runner's plumbing: it names the exact line, <-results, and the
function it's in. That's the whole diagnostic process for this class of
bug: find the blocked receive (or send) in the dump, then ask what was
supposed to unblock it and count how many times it actually will.
The fix is arithmetic, not architecture
for i := 0; i < n; i++ { // one receive per worker, no more
out = append(out, <-results)
}
Nothing about the channel, the goroutines, or the workers changes. The entire bug — and the entire fix — was matching the receive count to the send count exactly. That's worth sitting with: the most common real-world Go deadlock usually isn't a design flaw in how goroutines coordinate. It's this, in disguise.
Try it
Fix Collect so it gathers exactly n results and returns — for any n,
including zero.
Practice
1. Fix the Deadlock
⚑ Find the bugIntermediateCollectstartsngoroutines, each computingwork(i)and sending the result on a channel, then gathers allnresults into a slice.It deadlocks: the collection loop waits for one more result than any worker will ever send.
Fix
Collectso it receives exactlynresults (in whatever order they arrive) and returns, for anynincluding0. Don't change how the workers are started.package main // Collect starts n workers computing work(i) and gathers all n results. func Collect(n int, work func(int) int) []int { results := make(chan int) for i := 0; i < n; i++ { go func(i int) { results <- work(i) }(i) } out := make([]int, 0, n) for i := 0; i <= n; i++ { out = append(out, <-results) } return out } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.