Skip to content
Codeloom
Go

Go Concurrency Patterns

Master Go concurrency — goroutines, channels, select, WaitGroups, mutexes, and common patterns like fan-out/fan-in and worker pools.

·5 min read · By Codeloom
Advanced 14 min read

What you'll learn

  • How goroutines and channels enable concurrency in Go
  • Select statements for multiplexing channels
  • Fan-out/fan-in, worker pools, and pipeline patterns
  • When to use channels vs mutexes

Prerequisites

  • Go basics (functions, structs, interfaces)
  • Understanding of concurrent vs parallel execution

Go was designed with concurrency as a first-class citizen. Goroutines are cheap, channels are typed, and the select statement lets you multiplex. This guide covers the patterns you will use in production.

Goroutines

A goroutine is a lightweight thread managed by the Go runtime. Start one with the go keyword.

func main() {
    go sayHello("Alice")
    go sayHello("Bob")
    time.Sleep(time.Second) // wait for goroutines (don't do this in production)
}

func sayHello(name string) {
    fmt.Printf("Hello, %s!\n", name)
}

Goroutines cost about 2KB of stack (grows as needed) vs 1MB+ for OS threads. You can run millions of them.

Channels

Channels are typed conduits for communication between goroutines.

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

    go func() {
        ch <- "hello from goroutine"
    }()

    msg := <-ch
    fmt.Println(msg)
}

Buffered channels

Buffered channels allow sends without an immediate receiver, up to the buffer size.

ch := make(chan int, 3)
ch <- 1
ch <- 2
ch <- 3
// ch <- 4 would block — buffer full

Directional channels

Restrict channel direction in function signatures to prevent misuse.

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

func consumer(in <-chan int) {
    for val := range in {
        fmt.Println(val)
    }
}

Select

select waits on multiple channel operations. It is the concurrency equivalent of switch.

func main() {
    ch1 := make(chan string)
    ch2 := make(chan string)

    go func() {
        time.Sleep(100 * time.Millisecond)
        ch1 <- "one"
    }()

    go func() {
        time.Sleep(200 * time.Millisecond)
        ch2 <- "two"
    }()

    select {
    case msg := <-ch1:
        fmt.Println("Received from ch1:", msg)
    case msg := <-ch2:
        fmt.Println("Received from ch2:", msg)
    }
}

Timeout with select

select {
case result := <-ch:
    fmt.Println("Got result:", result)
case <-time.After(3 * time.Second):
    fmt.Println("Timed out")
}

WaitGroup

sync.WaitGroup waits for a collection of goroutines to finish.

func main() {
    var wg sync.WaitGroup
    urls := []string{
        "https://example.com",
        "https://example.org",
        "https://example.net",
    }

    for _, url := range urls {
        wg.Add(1)
        go func(u string) {
            defer wg.Done()
            resp, err := http.Get(u)
            if err != nil {
                fmt.Println("Error:", err)
                return
            }
            resp.Body.Close()
            fmt.Printf("%s%d\n", u, resp.StatusCode)
        }(url)
    }

    wg.Wait()
    fmt.Println("All requests complete")
}

Pattern: Fan-out / Fan-in

Fan-out: multiple goroutines read from the same channel. Fan-in: multiple channels feed into one.

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

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

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

    return merged
}

Pattern: Worker pool

A fixed number of goroutines process jobs from a shared channel.

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(id int) {
            defer wg.Done()
            for job := range jobs {
                fmt.Printf("Worker %d processing job %d\n", id, job)
                results <- job * 2
            }
        }(i)
    }

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

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

    workerPool(jobs, results, 5)

    for i := 0; i < 20; i++ {
        jobs <- i
    }
    close(jobs)

    for result := range results {
        fmt.Println("Result:", result)
    }
}

Pattern: Pipeline

Each stage is a goroutine that receives from one channel and sends to another.

func generate(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() {
    ch := generate(2, 3, 4, 5)
    result := square(ch)

    for val := range result {
        fmt.Println(val) // 4, 9, 16, 25
    }
}

Context for cancellation

The context package provides cancellation, deadlines, and request-scoped values.

func longRunning(ctx context.Context) error {
    for {
        select {
        case <-ctx.Done():
            return ctx.Err()
        default:
            // do work
            time.Sleep(100 * time.Millisecond)
        }
    }
}

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

    if err := longRunning(ctx); err != nil {
        fmt.Println("Stopped:", err) // context deadline exceeded
    }
}

Channels vs mutexes

Use caseChannelsMutexes
Passing data between goroutinesYesNo
Protecting shared statePossible but awkwardYes
Signaling eventsYesNo
Simple counters/cachesOverkillYes

The proverb: “Don’t communicate by sharing memory; share memory by communicating.” But use the right tool — a sync.Mutex protecting a map is simpler than a channel-based equivalent.

Summary

Go concurrency is built on goroutines, channels, and select. The patterns — fan-out/fan-in, worker pools, pipelines — compose from these primitives. Use sync.WaitGroup for simple coordination, context for cancellation, and reach for mutexes when protecting shared state is simpler than channel choreography.