Go Concurrency Patterns Best Practices¶
Objective: Master senior-level Go concurrency patterns for production systems. When you need to build robust, scalable concurrent applications, when you want to follow proven methodologies, when you need enterprise-grade concurrency patternsโthese best practices become your weapon of choice.
Core Principles¶
- Communicate by Sharing Memory: Use channels for communication, not shared memory
- Don't Share Memory: Avoid shared state, use message passing
- Goroutine Lifecycle Management: Proper creation, management, and cleanup
- Context-Driven Cancellation: Use context for graceful shutdown
- Backpressure Handling: Manage load and prevent resource exhaustion
Goroutine Patterns¶
Basic Goroutine Management¶
// internal/concurrency/worker.go
package concurrency
import (
"context"
"fmt"
"sync"
"time"
)
// WorkerPool manages a pool of workers
type WorkerPool struct {
workers int
jobQueue chan Job
resultChan chan Result
wg sync.WaitGroup
ctx context.Context
cancel context.CancelFunc
}
// Job represents a unit of work
type Job struct {
ID string
Data interface{}
}
// Result represents the result of a job
type Result struct {
JobID string
Data interface{}
Error error
}
// NewWorkerPool creates a new worker pool
func NewWorkerPool(workers int, bufferSize int) *WorkerPool {
ctx, cancel := context.WithCancel(context.Background())
return &WorkerPool{
workers: workers,
jobQueue: make(chan Job, bufferSize),
resultChan: make(chan Result, bufferSize),
ctx: ctx,
cancel: cancel,
}
}
// Start begins processing jobs
func (wp *WorkerPool) Start() {
for i := 0; i < wp.workers; i++ {
wp.wg.Add(1)
go wp.worker(i)
}
}
// worker processes jobs from the queue
func (wp *WorkerPool) worker(id int) {
defer wp.wg.Done()
for {
select {
case job := <-wp.jobQueue:
result := wp.processJob(job)
wp.resultChan <- result
case <-wp.ctx.Done():
return
}
}
}
// processJob handles individual job processing
func (wp *WorkerPool) processJob(job Job) Result {
// Simulate work
time.Sleep(100 * time.Millisecond)
return Result{
JobID: job.ID,
Data: fmt.Sprintf("Processed: %v", job.Data),
Error: nil,
}
}
// Submit adds a job to the queue
func (wp *WorkerPool) Submit(job Job) error {
select {
case wp.jobQueue <- job:
return nil
case <-wp.ctx.Done():
return fmt.Errorf("worker pool is shutting down")
}
}
// Results returns the result channel
func (wp *WorkerPool) Results() <-chan Result {
return wp.resultChan
}
// Stop gracefully shuts down the worker pool
func (wp *WorkerPool) Stop() {
wp.cancel()
close(wp.jobQueue)
wp.wg.Wait()
close(wp.resultChan)
}
Fan-Out/Fan-In Pattern¶
// internal/concurrency/fanout.go
package concurrency
import (
"context"
"sync"
)
// FanOut distributes work across multiple workers
func FanOut(ctx context.Context, input <-chan Job, workers int) []<-chan Result {
outputs := make([]<-chan Result, workers)
for i := 0; i < workers; i++ {
output := make(chan Result)
outputs[i] = output
go func(out chan<- Result) {
defer close(out)
for {
select {
case job, ok := <-input:
if !ok {
return
}
result := processJob(job)
out <- result
case <-ctx.Done():
return
}
}
}(output)
}
return outputs
}
// FanIn combines multiple channels into one
func FanIn(ctx context.Context, inputs ...<-chan Result) <-chan Result {
output := make(chan Result)
var wg sync.WaitGroup
for _, input := range inputs {
wg.Add(1)
go func(in <-chan Result) {
defer wg.Done()
for result := range in {
select {
case output <- result:
case <-ctx.Done():
return
}
}
}(input)
}
go func() {
wg.Wait()
close(output)
}()
return output
}
// processJob handles job processing
func processJob(job Job) Result {
// Implementation here
return Result{JobID: job.ID, Data: job.Data, Error: nil}
}
Channel Patterns¶
Pipeline Pattern¶
// internal/concurrency/pipeline.go
package concurrency
import (
"context"
"fmt"
)
// Pipeline represents a processing pipeline
type Pipeline struct {
stages []Stage
}
// Stage represents a pipeline stage
type Stage interface {
Process(ctx context.Context, input <-chan interface{}) <-chan interface{}
}
// TransformStage transforms data
type TransformStage struct {
transform func(interface{}) interface{}
}
// Process implements the Stage interface
func (ts *TransformStage) Process(ctx context.Context, input <-chan interface{}) <-chan interface{} {
output := make(chan interface{})
go func() {
defer close(output)
for {
select {
case data, ok := <-input:
if !ok {
return
}
transformed := ts.transform(data)
output <- transformed
case <-ctx.Done():
return
}
}
}()
return output
}
// FilterStage filters data
type FilterStage struct {
predicate func(interface{}) bool
}
// Process implements the Stage interface
func (fs *FilterStage) Process(ctx context.Context, input <-chan interface{}) <-chan interface{} {
output := make(chan interface{})
go func() {
defer close(output)
for {
select {
case data, ok := <-input:
if !ok {
return
}
if fs.predicate(data) {
output <- data
}
case <-ctx.Done():
return
}
}
}()
return output
}
// NewPipeline creates a new pipeline
func NewPipeline(stages ...Stage) *Pipeline {
return &Pipeline{stages: stages}
}
// Execute runs the pipeline
func (p *Pipeline) Execute(ctx context.Context, input <-chan interface{}) <-chan interface{} {
current := input
for _, stage := range p.stages {
current = stage.Process(ctx, current)
}
return current
}
Timeout and Cancellation¶
// internal/concurrency/timeout.go
package concurrency
import (
"context"
"time"
)
// WithTimeout creates a context with timeout
func WithTimeout(parent context.Context, timeout time.Duration) (context.Context, context.CancelFunc) {
return context.WithTimeout(parent, timeout)
}
// WithDeadline creates a context with deadline
func WithDeadline(parent context.Context, deadline time.Time) (context.Context, context.CancelFunc) {
return context.WithDeadline(parent, deadline)
}
// DoWithTimeout executes a function with timeout
func DoWithTimeout(ctx context.Context, fn func() error, timeout time.Duration) error {
ctx, cancel := WithTimeout(ctx, timeout)
defer cancel()
done := make(chan error, 1)
go func() {
done <- fn()
}()
select {
case err := <-done:
return err
case <-ctx.Done():
return ctx.Err()
}
}
// RetryWithBackoff retries a function with exponential backoff
func RetryWithBackoff(ctx context.Context, fn func() error, maxRetries int, initialDelay time.Duration) error {
var err error
for i := 0; i < maxRetries; i++ {
select {
case <-ctx.Done():
return ctx.Err()
default:
}
err = fn()
if err == nil {
return nil
}
if i < maxRetries-1 {
delay := time.Duration(1<<uint(i)) * initialDelay
time.Sleep(delay)
}
}
return err
}
Synchronization Patterns¶
Mutex Patterns¶
// internal/concurrency/sync.go
package concurrency
import (
"sync"
"time"
)
// SafeCounter is a thread-safe counter
type SafeCounter struct {
mu sync.RWMutex
count int
}
// Increment safely increments the counter
func (sc *SafeCounter) Increment() {
sc.mu.Lock()
defer sc.mu.Unlock()
sc.count++
}
// Get safely gets the counter value
func (sc *SafeCounter) Get() int {
sc.mu.RLock()
defer sc.mu.RUnlock()
return sc.count
}
// SafeMap is a thread-safe map
type SafeMap struct {
mu sync.RWMutex
m map[string]interface{}
}
// NewSafeMap creates a new thread-safe map
func NewSafeMap() *SafeMap {
return &SafeMap{
m: make(map[string]interface{}),
}
}
// Set safely sets a value
func (sm *SafeMap) Set(key string, value interface{}) {
sm.mu.Lock()
defer sm.mu.Unlock()
sm.m[key] = value
}
// Get safely gets a value
func (sm *SafeMap) Get(key string) (interface{}, bool) {
sm.mu.RLock()
defer sm.mu.RUnlock()
value, ok := sm.m[key]
return value, ok
}
// Delete safely deletes a value
func (sm *SafeMap) Delete(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.m, key)
}
Once Pattern¶
// internal/concurrency/once.go
package concurrency
import (
"sync"
"time"
)
// LazyInitializer provides lazy initialization
type LazyInitializer struct {
once sync.Once
data interface{}
err error
}
// Get initializes and returns data
func (li *LazyInitializer) Get(initFunc func() (interface{}, error)) (interface{}, error) {
li.once.Do(func() {
li.data, li.err = initFunc()
})
return li.data, li.err
}
// Singleton provides singleton pattern
type Singleton struct {
once sync.Once
instance *Singleton
}
// GetInstance returns the singleton instance
func (s *Singleton) GetInstance() *Singleton {
s.once.Do(func() {
s.instance = &Singleton{}
})
return s.instance
}
Advanced Patterns¶
Circuit Breaker¶
// internal/concurrency/circuitbreaker.go
package concurrency
import (
"context"
"errors"
"sync"
"time"
)
// CircuitState represents the state of the circuit breaker
type CircuitState int
const (
StateClosed CircuitState = iota
StateOpen
StateHalfOpen
)
// CircuitBreaker implements the circuit breaker pattern
type CircuitBreaker struct {
mu sync.RWMutex
state CircuitState
failures int
threshold int
timeout time.Duration
lastFailure time.Time
}
// NewCircuitBreaker creates a new circuit breaker
func NewCircuitBreaker(threshold int, timeout time.Duration) *CircuitBreaker {
return &CircuitBreaker{
state: StateClosed,
threshold: threshold,
timeout: timeout,
}
}
// Execute executes a function with circuit breaker protection
func (cb *CircuitBreaker) Execute(ctx context.Context, fn func() error) error {
if !cb.allowRequest() {
return errors.New("circuit breaker is open")
}
err := fn()
cb.recordResult(err)
return err
}
// allowRequest checks if a request should be allowed
func (cb *CircuitBreaker) allowRequest() bool {
cb.mu.RLock()
defer cb.mu.RUnlock()
switch cb.state {
case StateClosed:
return true
case StateOpen:
return time.Since(cb.lastFailure) > cb.timeout
case StateHalfOpen:
return true
default:
return false
}
}
// recordResult records the result of a request
func (cb *CircuitBreaker) recordResult(err error) {
cb.mu.Lock()
defer cb.mu.Unlock()
if err != nil {
cb.failures++
cb.lastFailure = time.Now()
if cb.failures >= cb.threshold {
cb.state = StateOpen
}
} else {
cb.failures = 0
cb.state = StateClosed
}
}
Rate Limiter¶
// internal/concurrency/ratelimiter.go
package concurrency
import (
"context"
"sync"
"time"
)
// RateLimiter implements token bucket rate limiting
type RateLimiter struct {
mu sync.Mutex
tokens int
capacity int
rate time.Duration
lastTime time.Time
}
// NewRateLimiter creates a new rate limiter
func NewRateLimiter(capacity int, rate time.Duration) *RateLimiter {
return &RateLimiter{
tokens: capacity,
capacity: capacity,
rate: rate,
lastTime: time.Now(),
}
}
// Allow checks if a request is allowed
func (rl *RateLimiter) Allow() bool {
rl.mu.Lock()
defer rl.mu.Unlock()
now := time.Now()
elapsed := now.Sub(rl.lastTime)
// Add tokens based on elapsed time
tokensToAdd := int(elapsed / rl.rate)
if tokensToAdd > 0 {
rl.tokens = min(rl.capacity, rl.tokens+tokensToAdd)
rl.lastTime = now
}
if rl.tokens > 0 {
rl.tokens--
return true
}
return false
}
// Wait waits for a token to become available
func (rl *RateLimiter) Wait(ctx context.Context) error {
for {
if rl.Allow() {
return nil
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(rl.rate):
// Try again
}
}
}
// min returns the minimum of two integers
func min(a, b int) int {
if a < b {
return a
}
return b
}
Error Handling in Concurrency¶
// internal/concurrency/errors.go
package concurrency
import (
"context"
"errors"
"fmt"
"sync"
)
// ConcurrencyError represents a concurrency-related error
type ConcurrencyError struct {
Op string
Err error
}
// Error implements the error interface
func (ce *ConcurrencyError) Error() string {
return fmt.Sprintf("concurrency error in %s: %v", ce.Op, ce.Err)
}
// Unwrap returns the underlying error
func (ce *ConcurrencyError) Unwrap() error {
return ce.Err
}
// ErrorGroup manages a group of goroutines with error handling
type ErrorGroup struct {
wg sync.WaitGroup
errs []error
mu sync.Mutex
ctx context.Context
cancel context.CancelFunc
}
// NewErrorGroup creates a new error group
func NewErrorGroup(ctx context.Context) *ErrorGroup {
ctx, cancel := context.WithCancel(ctx)
return &ErrorGroup{
ctx: ctx,
cancel: cancel,
}
}
// Go runs a function in a goroutine
func (eg *ErrorGroup) Go(fn func() error) {
eg.wg.Add(1)
go func() {
defer eg.wg.Done()
if err := fn(); err != nil {
eg.mu.Lock()
eg.errs = append(eg.errs, err)
eg.mu.Unlock()
eg.cancel()
}
}()
}
// Wait waits for all goroutines to complete
func (eg *ErrorGroup) Wait() error {
eg.wg.Wait()
eg.cancel()
if len(eg.errs) > 0 {
return fmt.Errorf("multiple errors: %v", eg.errs)
}
return nil
}
Testing Concurrency¶
// internal/concurrency/concurrency_test.go
package concurrency
import (
"context"
"testing"
"time"
)
func TestWorkerPool(t *testing.T) {
pool := NewWorkerPool(3, 10)
pool.Start()
defer pool.Stop()
// Submit jobs
for i := 0; i < 5; i++ {
job := Job{
ID: fmt.Sprintf("job-%d", i),
Data: fmt.Sprintf("data-%d", i),
}
err := pool.Submit(job)
if err != nil {
t.Fatalf("Failed to submit job: %v", err)
}
}
// Collect results
results := make([]Result, 0, 5)
for i := 0; i < 5; i++ {
select {
case result := <-pool.Results():
results = append(results, result)
case <-time.After(5 * time.Second):
t.Fatal("Timeout waiting for results")
}
}
if len(results) != 5 {
t.Errorf("Expected 5 results, got %d", len(results))
}
}
func TestCircuitBreaker(t *testing.T) {
cb := NewCircuitBreaker(2, 100*time.Millisecond)
// Test successful execution
err := cb.Execute(context.Background(), func() error {
return nil
})
if err != nil {
t.Errorf("Expected no error, got %v", err)
}
// Test failure threshold
for i := 0; i < 3; i++ {
err := cb.Execute(context.Background(), func() error {
return errors.New("test error")
})
if i < 2 && err == nil {
t.Error("Expected error for failed execution")
}
if i >= 2 && err == nil {
t.Error("Expected circuit breaker to be open")
}
}
}
TL;DR Runbook¶
Quick Start¶
// 1. Basic goroutine
go func() {
// Your code here
}()
// 2. Worker pool
pool := NewWorkerPool(4, 100)
pool.Start()
defer pool.Stop()
// 3. Pipeline
pipeline := NewPipeline(
&TransformStage{transform: func(x interface{}) interface{} { return x }},
&FilterStage{predicate: func(x interface{}) bool { return true }},
)
// 4. Circuit breaker
cb := NewCircuitBreaker(5, time.Minute)
err := cb.Execute(ctx, func() error { return nil })
Essential Patterns¶
// Context cancellation
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
// Fan-out/fan-in
outputs := FanOut(ctx, input, 4)
result := FanIn(ctx, outputs...)
// Rate limiting
limiter := NewRateLimiter(100, time.Second)
if !limiter.Allow() {
return errors.New("rate limited")
}
This guide provides the complete machinery for building production-ready concurrent Go applications. Each pattern includes implementation examples, testing strategies, and real-world usage patterns for enterprise deployment.