Go Concurrency Patterns Cheat Sheet
Covers goroutines, channels, select statements, sync primitives, and common concurrency patterns like worker pools and fan-in/fan-out in Go.
Goroutines & Channels
Starting goroutines and communicating over channels.
func main() { ch := make(chan string) go func() { ch <- "hello from goroutine" }() msg := <-ch fmt.Println(msg) // Buffered channel buf := make(chan int, 3) buf <- 1 buf <- 2 close(buf) for v := range buf { fmt.Println(v) // ranges until channel is closed }}
Select Statement
Waiting on multiple channel operations at once.
select {case msg1 := <-ch1: fmt.Println("received", msg1)case msg2 := <-ch2: fmt.Println("received", msg2)case <-time.After(1 * time.Second): fmt.Println("timeout")default: fmt.Println("no message ready") // non-blocking select}
sync Package
Core synchronization primitives from the standard library.
- sync.Mutex- Locks a critical section; call Lock() and Unlock() (typically via defer)
- sync.RWMutex- Allows multiple concurrent readers or one writer; RLock()/RUnlock() for reads
- sync.WaitGroup- Waits for a collection of goroutines to finish via Add/Done/Wait
- sync.Once- Ensures a function runs exactly once, e.g. for lazy initialization
- sync.Map- Concurrency-safe map optimized for specific high-contention access patterns
- atomic package- Lock-free atomic operations (AddInt64, LoadInt32, etc.) from sync/atomic
- context.Context- Carries cancellation signals and deadlines across API boundaries and goroutines
Worker Pool Pattern
Fixed pool of goroutines processing jobs from a shared channel.
func main() { jobs := make(chan int, 100) results := make(chan int, 100) var wg sync.WaitGroup for w := 1; w <= 3; w++ { wg.Add(1) go func(id int) { defer wg.Done() for j := range jobs { results <- j * j } }(w) } for j := 1; j <= 9; j++ { jobs <- j } close(jobs) wg.Wait() close(results) for r := range results { fmt.Println(r) }}
Common Patterns
Named concurrency patterns you'll see across Go codebases.
- Fan-out- Multiple goroutines read from the same channel to parallelize work
- Fan-in- Merge multiple channels into one using a dedicated merge goroutine
- Pipeline- Chain stages so each stage's output channel feeds the next stage's input
- Done channel- A closed channel broadcasts cancellation to every listening goroutine
- context.WithCancel- Standard way to propagate cancellation through a call graph
- context.WithTimeout- Cancels automatically after a duration; use with select on ctx.Done()
errgroup for Coordinated Cancellation
Running a group of goroutines that share an error and a cancellation context.
import "golang.org/x/sync/errgroup"func FetchAll(ctx context.Context, urls []string) ([]string, error) { g, ctx := errgroup.WithContext(ctx) results := make([]string, len(urls)) for i, url := range urls { i, url := i, url g.Go(func() error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err // first error cancels ctx for all other goroutines } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) results[i] = string(body) return err }) } if err := g.Wait(); err != nil { return nil, err } return results, nil}
context.Context: Cancellation Trees & Values
Building a cancellation hierarchy and passing request-scoped data safely.
type ctxKey stringconst requestIDKey ctxKey = "requestID"func handle(ctx context.Context) { ctx = context.WithValue(ctx, requestIDKey, "req-123") // Use unexported key types ctx, cancel := context.WithTimeout(ctx, 2*time.Second) defer cancel() // Always call cancel to release resources, even on success select { case <-doWork(ctx): fmt.Println("done, requestID:", ctx.Value(requestIDKey)) case <-ctx.Done(): fmt.Println("cancelled:", ctx.Err()) // context.DeadlineExceeded or context.Canceled }}// Cancelling a parent ctx cancels every ctx derived from it (a tree),// but cancelling a child never affects its parent.
sync.Cond & sync.Pool
Two less common sync primitives: condition variables and object reuse.
// sync.Cond -- wait for a condition guarded by a mutexvar ( mu sync.Mutex cond = sync.NewCond(&mu) ready bool)func waiter() { mu.Lock() for !ready { // Always re-check the condition in a loop (spurious wakeups) cond.Wait() } mu.Unlock()}func signaler() { mu.Lock() ready = true mu.Unlock() cond.Broadcast() // Wake all waiters; cond.Signal() wakes just one}// sync.Pool -- reuse short-lived objects to reduce GC pressurevar bufPool = sync.Pool{ New: func() interface{} { return new(bytes.Buffer) },}func render() { buf := bufPool.Get().(*bytes.Buffer) defer func() { buf.Reset(); bufPool.Put(buf) }() buf.WriteString("rendered output")}
Cancellable Pipeline with Fan-Out/Fan-In
Combining pipeline stages with a done channel so no goroutine leaks on early exit.
func generator(done <-chan struct{}, nums ...int) <-chan int { out := make(chan int) go func() { defer close(out) for _, n := range nums { select { case out <- n: case <-done: return // Abandon work as soon as the consumer signals it's done } } }() return out}func square(done <-chan struct{}, in <-chan int) <-chan int { out := make(chan int) go func() { defer close(out) for n := range in { select { case out <- n * n: case <-done: return } } }() return out}func main() { done := make(chan struct{}) defer close(done) // Broadcasts cancellation to every stage still running for n := range square(done, generator(done, 1, 2, 3, 4)) { fmt.Println(n) break // Early exit -- upstream goroutines unblock via done, not deadlock }}
Concurrency Gotchas & Diagnostics
Failure modes and tooling that go beyond the basic goroutine/channel primer.
- Goroutine leaks- A goroutine blocked forever on a channel send/receive with no reader/writer never gets garbage collected; always give it a cancellation path
- nil channel behavior- Sending/receiving on a nil channel blocks forever; useful in a select to conditionally disable a case
- Closed channel semantics- Receiving from a closed channel returns the zero value immediately with ok=false; sending to a closed channel panics
- GOMAXPROCS- Controls the number of OS threads executing Go code simultaneously; runtime.GOMAXPROCS(n) tunes it, defaults to NumCPU
- go build -race / go run -race- Instruments memory accesses to detect data races at runtime; essential for validating concurrent code, not just go test
- runtime.Gosched()- Voluntarily yields the processor to other goroutines without blocking; rarely needed but useful in busy-loop scenarios
- Deadlock detection- The Go runtime detects when ALL goroutines are asleep (global deadlock) and panics with "fatal error: all goroutines are asleep"
- errgroup.WithContext- Derives a context that's cancelled the moment any goroutine in the group returns a non-nil error
Always know who closes a channel and only close it from the sender side — closing a channel you're only receiving from, or closing it twice, causes a panic.