Skip to main content
โšก Calmops

Golang Concurrency Patterns: Mastering Goroutines and Channels

Go (Golang) is renowned for its simplicity and power in handling concurrency. At the heart of Go’s concurrency model are goroutinesโ€”lightweight threadsโ€”and channels for communication. Unlike traditional threading models, Go encourages sharing memory by communicating, leading to safer and more maintainable code. This post explores key concurrency patterns in Go, providing explanations and code examples for each.

Introduction to Go Concurrency

Go’s concurrency primitives make it easy to write programs that do multiple things simultaneously. Goroutines are created with the go keyword and run concurrently. Channels allow goroutines to communicate safely. Patterns build on these to solve common problems like parallel processing, data pipelines, and synchronization.

Basic Patterns

1. Goroutines and Channels

Goroutines run functions concurrently, and channels send/receive data.

Example:

package main

import (
    "fmt"
    "time"
)

func say(s string) {
    for i := 0; i < 5; i++ {
        time.Sleep(100 * time.Millisecond)
        fmt.Println(s)
    }
}

func main() {
    go say("world")
    say("hello")
}

This runs “hello” and “world” concurrently.

2. Worker Pool

Distribute tasks among a fixed number of workers.

Example:

package main

import (
    "fmt"
    "time"
)

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

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

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

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

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

Workers process jobs concurrently.

3. Pipeline

Chain operations where each stage processes and passes data.

Example:

package main

import "fmt"

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

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

func main() {
    c := gen(2, 3)
    out := sq(c)
    fmt.Println(<-out) // 4
    fmt.Println(<-out) // 9
}

Data flows from gen to sq.

4. Fan-In/Fan-Out

Fan-out distributes work, fan-in collects results.

Example:

package main

import (
    "fmt"
    "sync"
)

func producer(ch chan<- int) {
    for i := 0; i < 10; i++ {
        ch <- i
    }
    close(ch)
}

func consumer(id int, ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for val := range ch {
        fmt.Printf("Consumer %d: %d\n", id, val)
    }
}

func main() {
    ch := make(chan int, 10)
    var wg sync.WaitGroup

    go producer(ch)

    for i := 1; i <= 3; i++ {
        wg.Add(1)
        go consumer(i, ch, &wg)
    }

    wg.Wait()
}

Producer fans out to consumers.

5. Select for Multiplexing

Handle multiple channels with select.

Example:

package main

import (
    "fmt"
    "time"
)

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)
        }
    }
}

Selects the first ready channel.

6. Context for Cancellation

Use context to cancel operations.

Example:

package main

import (
    "context"
    "fmt"
    "time"
)

func worker(ctx context.Context, id int) {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Worker %d cancelled\n", id)
            return
        default:
            fmt.Printf("Worker %d working\n", id)
            time.Sleep(500 * time.Millisecond)
        }
    }
}

func main() {
    ctx, cancel := context.WithCancel(context.Background())

    for i := 1; i <= 3; i++ {
        go worker(ctx, i)
    }

    time.Sleep(2 * time.Second)
    cancel()
    time.Sleep(1 * time.Second)
}

Cancels workers gracefully.

7. Mutex for Shared State

Though not purely concurrency, use sync.Mutex for shared data.

Example:

package main

import (
    "fmt"
    "sync"
)

type Counter struct {
    mu    sync.Mutex
    value int
}

func (c *Counter) Inc() {
    c.mu.Lock()
    c.value++
    c.mu.Unlock()
}

func main() {
    c := Counter{}
    var wg sync.WaitGroup

    for i := 0; i < 100; i++ {
        wg.Add(1)
        go func() {
            c.Inc()
            wg.Done()
        }()
    }

    wg.Wait()
    fmt.Println(c.value) // 100
}

Safely increments counter.

Best Practices

  • Prefer channels over shared memory.
  • Use buffered channels for performance.
  • Handle panics in goroutines.
  • Profile with go tool pprof.

Conclusion

Go’s concurrency patterns simplify parallel programming. Start with basics, then apply patterns like worker pools for scalability. Experiment with these examples to build robust concurrent applications.

For more, read “Concurrency in Go” by Katherine Cox-Buday or the Go documentation.

Comments