Concurrency Design Guidelines

Overview

Concurrency can introduce subtle bugs. Follow these guidelines to write safe concurrent code.

Share by Communicating

// Prefer: communicate over channels
result := <-worker.Results()

// Avoid: shared memory with locks
mu.Lock()
result := sharedData
mu.Unlock()

Avoid Goroutine Leaks

// Bad: goroutine leaks if nobody receives
go func() {
    ch <- result  // Blocks forever
}()

// Good: use context cancellation
go func() {
    select {
    case ch <- result:
    case <-ctx.Done():
    }
}()

Always Close Channels from Sender

// Producer closes
func producer(out chan<- int) {
    for i := 0; i < 10; i++ {
        out <- i
    }
    close(out)  // Only sender closes
}

// Consumer ranges
for v := range in {
    process(v)
}

Race Detection

go test -race ./...
go run -race main.go

Common Patterns

Worker Pool

jobs := make(chan Job)
for i := 0; i < workers; i++ {
    go worker(jobs)
}

Bounded Concurrency

sem := make(chan struct{}, maxConcurrent)

for _, task := range tasks {
    sem <- struct{}{}
    go func(t Task) {
        defer func() { <-sem }()
        process(t)
    }(task)
}

Deadlock Prevention

// Deadlock: circular wait
ch := make(chan int)
ch <- 1    // Blocks: no receiver
<-ch       // Never reached

// Fix: buffer or separate goroutine
ch := make(chan int, 1)
ch <- 1
<-ch

Summary

Guideline Reason
Share by communicating Clearer ownership
Use context for cancellation Prevent leaks
Only sender closes Avoid panic
Run race detector Catch data races