Go’s concurrency model is one of its most powerful features. This guide explores goroutines, channels, and advanced patterns for writing efficient concurrent code.

Goroutines Basics

Goroutines are lightweight threads managed by the Go runtime:

package main

import (
    "fmt"
    "time"
)

func main() {
    // Start goroutine
    go sayHello("world")

    // Anonymous goroutine
    go func() {
        fmt.Println("Anonymous function")
    }()

    time.Sleep(time.Second)
}

func sayHello(s string) {
    fmt.Println("Hello", s)
}

Channels

Channels enable communication between goroutines:

func main() {
    messages := make(chan string)

    // Send in goroutine
    go func() {
        messages <- "ping"
    }()

    // Receive
    msg := <-messages
    fmt.Println(msg)
}

Buffered Channels

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

ch <- 1
ch <- 2
// ch <- 3  // Would block

fmt.Println(<-ch)  // 1
fmt.Println(<-ch)  // 2

Worker Pool Pattern

func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Printf("Worker %d processing job %d\n", id, j)
        time.Sleep(time.Second)
        results <- j * 2
    }
}

func main() {
    jobs := make(chan int, 100)
    results := make(chan int, 100)

    // Start workers
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    // Send jobs
    for j := 1; j <= 9; j++ {
        jobs <- j
    }
    close(jobs)

    // Collect results
    for a := 1; a <= 9; a++ {
        <-results
    }
}

Fan-Out, Fan-In

func fanOut(input <-chan int, workers int) []<-chan int {
    channels := make([]<-chan int, workers)
    for i := 0; i < workers; i++ {
        channels[i] = worker(input)
    }
    return channels
}

func fanIn(channels ...<-chan int) <-chan int {
    out := make(chan int)
    var wg sync.WaitGroup

    for _, ch := range channels {
        wg.Add(1)
        go func(c <-chan int) {
            defer wg.Done()
            for n := range c {
                out <- n
            }
        }(ch)
    }

    go func() {
        wg.Wait()
        close(out)
    }()

    return out
}

Select Statement

func main() {
    c1 := make(chan string)
    c2 := make(chan string)

    go func() {
        time.Sleep(1 * time.Second)
        c1 <- "one"
    }()

    go func() {
        time.Sleep(2 * time.Second)
        c2 <- "two"
    }()

    for i := 0; i < 2; i++ {
        select {
        case msg1 := <-c1:
            fmt.Println("Received", msg1)
        case msg2 := <-c2:
            fmt.Println("Received", msg2)
        case <-time.After(3 * time.Second):
            fmt.Println("Timeout")
        }
    }
}

Context for Cancellation

func operation(ctx context.Context) error {
    select {
    case <-time.After(5 * time.Second):
        return nil
    case <-ctx.Done():
        return ctx.Err()
    }
}

func main() {
    ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
    defer cancel()

    if err := operation(ctx); err != nil {
        fmt.Println("Operation cancelled:", err)
    }
}

Pipeline Pattern

func generator(nums ...int) <-chan int {
    out := make(chan int)
    go func() {
        for _, n := range nums {
            out <- n
        }
        close(out)
    }()
    return out
}

func square(in <-chan int) <-chan int {
    out := make(chan int)
    go func() {
        for n := range in {
            out <- n * n
        }
        close(out)
    }()
    return out
}

func main() {
    // Pipeline
    for n := range square(generator(1, 2, 3, 4)) {
        fmt.Println(n)
    }
}

Conclusion

Go’s concurrency primitives make it easy to write efficient parallel code. Master these patterns for building scalable applications.