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]: or goroutine 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. 1.Use select with default to avoid blocking:
  2. 2.```go
  3. 3.// WRONG - blocks forever if no receiver
  4. 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. 1.Use buffered channels for producer-consumer patterns:
  2. 2.```go
  3. 3.// WRONG - unbuffered, producer blocks if consumer is slow
  4. 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. 1.Properly close channels when done:
  2. 2.```go
  3. 3.func producer(ch chan<- int, items []int) {
  4. 4.defer close(ch) // Signal completion
  5. 5.for _, item := range items {
  6. 6.ch <- item
  7. 7.}
  8. 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. 1.Use context for cancellation to prevent deadlock:
  2. 2.```go
  3. 3.func worker(ctx context.Context, input <-chan int, output chan<- int) {
  4. 4.for {
  5. 5.select {
  6. 6.case <-ctx.Done():
  7. 7.return // Exit on cancellation
  8. 8.case val, ok := <-input:
  9. 9.if !ok {
  10. 10.return // Channel closed
  11. 11.}
  12. 12.select {
  13. 13.case output <- val * 2:
  14. 14.case <-ctx.Done():
  15. 15.return
  16. 16.}
  17. 17.}
  18. 18.}
  19. 19.}

ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) defer cancel()

go worker(ctx, inputCh, outputCh) ```

  1. 1.Detect deadlock in tests with timeout:
  2. 2.```go
  3. 3.func TestNoDeadlock(t *testing.T) {
  4. 4.done := make(chan struct{})
  5. 5.go func() {
  6. 6.runPipeline() // Your concurrent code
  7. 7.close(done)
  8. 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 select with context.Done() for cancellable operations
  • Run with go run -race to detect channel issues
  • Use go tool trace to visualize goroutine scheduling
  • Test concurrent code with -race flag and extended timeouts
  • Use the deadlock package (github.com/sasha-s/go-deadlock) in development