Skip to content
Codeloom

Courses / Go Fundamentals

Lesson 13 of 18

Go Concurrency Patterns

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

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.

Progress is saved locally to your browser.