Go makes concurrency feel easy — goroutines are cheap, channels are built in, and you can spin up a worker pool in under 30 lines. But that same ease is why subtle bugs slip through: the code compiles, tests pass, and the problem only shows up under load.
After years on high-throughput Go backends, I keep running into the same three:
Goroutine leaks from channels that never close. If the producer stops sending but never calls close(), your workers block forever on for range — quietly holding memory. Fix: the producer owns the channel and closes it with defer close().
Calling wg.Add inside the goroutine. Do that and Wait() can hit zero before any worker registers, so main exits while work is still running — dropped silently. Fix: call wg.Add before launching the goroutine (or Add(N) up front).
Bidirectional channels everywhere. Plain chan Job lets a worker accidentally write to the input channel and compile clean. Fix: use chan<- and <-chan at function boundaries — free, compiler-enforced documentation.
None of these need clever solutions. They need habit: producer owns close, Add precedes launch, direction is documentation.
Bonus: Go 1.25’s go vet now flags mistake #2 automatically.
Full write-up here https://medium.com/stackademic/three-go-concurrency-mistakes-i-see-in-almost-every-worker-pool-480a7de51c43
#Golang #Concurrency #BackendDevelopment #SoftwareEngineering
