Introduction
Unbuffered channels require both a sender and receiver to be ready simultaneously. If a goroutine tries to send on an unbuffered channel but no goroutine is receiving (or vice versa), the sender blocks forever. When all goroutines are blocked, the Go runtime detects the deadlock and panics.
Symptoms
fatal error: all goroutines are asleep - deadlock!goroutine 1 [chan send]:orgoroutine 1 [chan receive]:- Program hangs indefinitely without error (if not main goroutine)
- Works with small input but deadlocks with larger input
- Deadlock only occurs on certain execution paths
``` fatal error: all goroutines are asleep - deadlock!
goroutine 1 [chan send]: main.main() /app/main.go:15 +0x5a
goroutine 18 [chan receive]: main.worker(...) /app/main.go:25 ```
Common Causes
- Sending to channel in a loop but receiver exits early on error
- Channel created but no goroutine ever reads from it
- Multiple senders competing, some blocked forever
- Closing channel while goroutine still sending to it
- Forgetting to close channel, causing range to block forever
Step-by-Step Fix
- 1.Use select with default to avoid blocking:
- 2.```go
- 3.// WRONG - blocks forever if no receiver
- 4.ch <- result
// CORRECT - non-blocking send with select select { case ch <- result: // sent successfully default: // channel full or no receiver, handle gracefully log.Println("dropped result - no receiver available") } ```
- 1.Use buffered channels for producer-consumer patterns:
- 2.```go
- 3.// WRONG - unbuffered, producer blocks if consumer is slow
- 4.ch := make(chan int)
// CORRECT - buffered, allows producer to get ahead ch := make(chan int, 100)
// Even better - size based on expected workload ch := make(chan int, numWorkers) ```
- 1.Properly close channels when done:
- 2.```go
- 3.func producer(ch chan<- int, items []int) {
- 4.defer close(ch) // Signal completion
- 5.for _, item := range items {
- 6.ch <- item
- 7.}
- 8.}
func consumer(ch <-chan int) { for item := range ch { // Exits when channel is closed process(item) } }
// Usage ch := make(chan int, 10) go producer(ch, []int{1, 2, 3}) consumer(ch) // Blocks until producer closes ch ```
- 1.Use context for cancellation to prevent deadlock:
- 2.```go
- 3.func worker(ctx context.Context, input <-chan int, output chan<- int) {
- 4.for {
- 5.select {
- 6.case <-ctx.Done():
- 7.return // Exit on cancellation
- 8.case val, ok := <-input:
- 9.if !ok {
- 10.return // Channel closed
- 11.}
- 12.select {
- 13.case output <- val * 2:
- 14.case <-ctx.Done():
- 15.return
- 16.}
- 17.}
- 18.}
- 19.}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()
go worker(ctx, inputCh, outputCh) ```
- 1.Detect deadlock in tests with timeout:
- 2.```go
- 3.func TestNoDeadlock(t *testing.T) {
- 4.done := make(chan struct{})
- 5.go func() {
- 6.runPipeline() // Your concurrent code
- 7.close(done)
- 8.}()
select { case <-done: // Test passed case <-time.After(5 * time.Second): t.Fatal("deadlock detected - test timed out") } } ```
Prevention
- Prefer buffered channels with capacity matching expected throughput
- Always close channels from the sender side when done
- Use
selectwithcontext.Done()for cancellable operations - Run with
go run -raceto detect channel issues - Use
go tool traceto visualize goroutine scheduling - Test concurrent code with
-raceflag and extended timeouts - Use the
deadlockpackage (github.com/sasha-s/go-deadlock) in development