main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110
package main

import (
	"context"
	"fmt"
	"sync"
	"time"
)

// DeadlockDetector monitors goroutine communication for deadlocks
type DeadlockDetector struct {
	timeout time.Duration
	wg      sync.WaitGroup
	ctx     context.Context
	cancel  context.CancelFunc
}

// NewDeadlockDetector creates a new detector with specified timeout
func NewDeadlockDetector(timeout time.Duration) *DeadlockDetector {
	ctx, cancel := context.WithCancel(context.Background())
	return &DeadlockDetector{
		timeout: timeout,
		ctx:     ctx,
		cancel:  cancel,
	}
}

// MonitorGoroutine tracks a goroutine for potential deadlock
func (d *DeadlockDetector) MonitorGoroutine(name string, fn func()) error {
	d.wg.Add(1)
	errChan := make(chan error, 1)

	go func() {
		defer d.wg.Done()
		defer close(errChan)

		// Create timeout context for this goroutine
		ctx, cancel := context.WithTimeout(d.ctx, d.timeout)
		defer cancel()

		// Channel to track completion
		done := make(chan struct{}, 1)

		// Run the target function in a separate goroutine
		go func() {
			fn()
			done <- struct{}{}
		}()

		// Wait for either completion or timeout
		select {
		case <-done:
			// Success - no deadlock
			return
		case <-ctx.Done():
			errChan <- fmt.Errorf("goroutine '%s' deadlock detected: %w", name, ctx.Err())
			return
		}
	}()

	// Wait for result or parent cancellation
	select {
	case err := <-errChan:
		return err
	case <-d.ctx.Done():
		return fmt.Errorf("monitoring cancelled for goroutine '%s': %w", name, d.ctx.Err())
	}
}

// Stop terminates all monitoring and waits for cleanup
func (d *DeadlockDetector) Stop() {
	d.cancel()
	d.wg.Wait()
}

// Example deadlock scenario: unbuffered channel send without receiver
func deadlockExample() {
	ch := make(chan int)
	// This will deadlock - sending to unbuffered channel with no receiver
	ch <- 42
}

// Fixed version with proper synchronization
func noDeadlockExample() {
	ch := make(chan int, 1) // Buffered channel prevents deadlock
	ch <- 42
	close(ch)
}

func main() {
	// Create detector with 2-second timeout
	detector := NewDeadlockDetector(2 * time.Second)
	defer detector.Stop()

	// Test deadlock scenario
	fmt.Println("Testing deadlock scenario...")
	err := detector.MonitorGoroutine("deadlock-example", deadlockExample)
	if err != nil {
		fmt.Printf("Error: %v\n", err) // Should show deadlock error
	}

	// Test non-deadlock scenario
	fmt.Println("\nTesting non-deadlock scenario...")
	err = detector.MonitorGoroutine("no-deadlock-example", noDeadlockExample)
	if err != nil {
		fmt.Printf("Error: %v\n", err)
	} else {
		fmt.Println("No deadlock detected!")
	}
}

How It Works

Illustrates deadlock-prone channel patterns and a detector that uses timeouts to surface stuck goroutines.

Runs goroutines that intentionally block on channels, wraps operations in select with context deadlines, and reports when operations exceed expected durations to highlight deadlocks.

Key Concepts

  • 1Shows classic send or receive ordering issues that freeze pipelines.
  • 2Context with timeout guards channel operations.
  • 3Detector prints diagnostics when deadlines hit.

When to Use This Pattern

  • Teaching concurrency pitfalls.
  • Adding watchdogs around channel-based systems.
  • Reproducing deadlocks in tests to validate fixes.

Best Practices

  • Always consider select with timeout when dealing with channels.
  • Avoid holding locks while performing blocking channel operations.
  • Prefer bounded buffers to reduce risk of circular waits.
Go Version1.17
Difficultyadvanced
Production ReadyYes
Lines of Code110