Go’s concurrency model is one of its strongest features. Here’s how to effectively use goroutines, channels, and concurrency patterns.

Goroutines

Basic Usage

// Simple goroutine
go func() {
    fmt.Println("Running in goroutine")
}()

// With function
go processData(data)

WaitGroup

import "sync"

var wg sync.WaitGroup

func main() {
    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()
            processItem(id)
        }(i)
    }
    wg.Wait() // Wait for all goroutines
}

Channels

Basic Channel Operations

// Unbuffered channel
ch := make(chan int)

// Send
ch <- 42

// Receive
value := <-ch

// Close
close(ch)

Buffered Channels

// Buffered channel (capacity 10)
ch := make(chan int, 10)

// Non-blocking send if buffer not full
ch <- 1
ch <- 2

Channel Directions

// Send-only channel
func sendOnly(ch chan<- int) {
    ch <- 42
}

// Receive-only channel
func receiveOnly(ch <-chan int) {
    value := <-ch
}

// Bidirectional (default)
func bidirectional(ch chan int) {
    ch <- 42
    value := <-ch
}

Common Patterns

Worker Pool

func workerPool(jobs <-chan int, results chan<- int, numWorkers int) {
    var wg sync.WaitGroup
    
    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func() {
            defer wg.Done()
            for job := range jobs {
                result := processJob(job)
                results <- result
            }
        }()
    }
    
    wg.Wait()
    close(results)
}

Fan-Out, Fan-In

// Fan-out: Distribute work
func fanOut(input <-chan int, outputs []chan int) {
    for value := range input {
        for _, output := range outputs {
            output <- value
        }
    }
    for _, output := range outputs {
        close(output)
    }
}

// Fan-in: Combine results
func fanIn(inputs []<-chan int) <-chan int {
    output := make(chan int)
    var wg sync.WaitGroup
    
    for _, input := range inputs {
        wg.Add(1)
        go func(ch <-chan int) {
            defer wg.Done()
            for value := range ch {
                output <- value
            }
        }(input)
    }
    
    go func() {
        wg.Wait()
        close(output)
    }()
    
    return output
}

Pipeline Pattern

func pipeline(input <-chan int) <-chan int {
    stage1 := make(chan int)
    stage2 := make(chan int)
    
    // Stage 1
    go func() {
        defer close(stage1)
        for value := range input {
            stage1 <- value * 2
        }
    }()
    
    // Stage 2
    go func() {
        defer close(stage2)
        for value := range stage1 {
            stage2 <- value + 1
        }
    }()
    
    return stage2
}

Context for Cancellation

Using Context

import "context"

func processWithContext(ctx context.Context, data []int) error {
    for _, item := range data {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            processItem(item)
        }
    }
    return nil
}

// Usage
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()

err := processWithContext(ctx, data)

Context with Timeout

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

result, err := fetchData(ctx)

Select Statement

Non-Blocking Operations

select {
case value := <-ch1:
    // Handle value from ch1
case value := <-ch2:
    // Handle value from ch2
case ch3 <- 42:
    // Successfully sent to ch3
default:
    // No channel ready
}

Timeout Pattern

select {
case result := <-ch:
    return result
case <-time.After(5 * time.Second):
    return errors.New("timeout")
}

Mutex and RWMutex

Protecting Shared State

type SafeCounter struct {
    mu    sync.Mutex
    value int
}

func (c *SafeCounter) Increment() {
    c.mu.Lock()
    defer c.mu.Unlock()
    c.value++
}

func (c *SafeCounter) Value() int {
    c.mu.Lock()
    defer c.mu.Unlock()
    return c.value
}

Read-Write Mutex

type SafeMap struct {
    mu   sync.RWMutex
    data map[string]int
}

func (m *SafeMap) Get(key string) int {
    m.mu.RLock()
    defer m.mu.RUnlock()
    return m.data[key]
}

func (m *SafeMap) Set(key string, value int) {
    m.mu.Lock()
    defer m.mu.Unlock()
    m.data[key] = value
}

Best Practices

1. Always Use defer for Cleanup

// Good
func process() {
    mu.Lock()
    defer mu.Unlock()
    // Process
}

// Bad
func process() {
    mu.Lock()
    // Process
    mu.Unlock() // Might not execute if panic
}

2. Avoid Goroutine Leaks

// Good: Use context for cancellation
func worker(ctx context.Context) {
    for {
        select {
        case <-ctx.Done():
            return
        default:
            // Work
        }
    }
}

// Bad: Goroutine runs forever
func worker() {
    for {
        // Work forever
    }
}

3. Close Channels Properly

// Good: Close channel when done
func producer(ch chan int) {
    defer close(ch)
    for i := 0; i < 10; i++ {
        ch <- i
    }
}

Common Pitfalls

1. Race Conditions

// Bad: Race condition
var counter int

func increment() {
    counter++ // Not thread-safe
}

// Good: Use mutex
var (
    counter int
    mu      sync.Mutex
)

func increment() {
    mu.Lock()
    defer mu.Unlock()
    counter++
}

2. Closing Closed Channel

// Bad: Panic if channel already closed
close(ch)
close(ch) // Panic!

// Good: Check or use sync.Once
var once sync.Once
once.Do(func() { close(ch) })

Performance Tips

1. Use Buffered Channels

// For known capacity
ch := make(chan int, 100)

2. Limit Goroutine Count

// Use worker pool instead of unlimited goroutines
const maxWorkers = 10
semaphore := make(chan struct{}, maxWorkers)

3. Profile Your Code

import _ "net/http/pprof"

// Add to main
go func() {
    log.Println(http.ListenAndServe("localhost:6060", nil))
}()

Conclusion

Go’s concurrency model provides:

  • Goroutines: Lightweight threads
  • Channels: Safe communication
  • Select: Multiplexing
  • Context: Cancellation and timeouts

Key principles:

  • Don’t communicate by sharing memory; share memory by communicating
  • Use channels for communication
  • Use mutexes only when necessary
  • Always handle context cancellation
  • Avoid goroutine leaks

Master these patterns to write efficient, concurrent Go programs! 🚀