正在加载,请稍候…

Go Concurrency Patterns in 2026: Goroutines, Channels, and Structured Concurrency

Master Go concurrency in 2026 with practical patterns for goroutine leak detection, channel direction, select timeouts, errgroup, context cancellation, worker pools, and fan-out/fan-in architectures.

Go Concurrency Patterns in 2026: Goroutines, Channels, and Structured Concurrency

Go's concurrency model remains one of the language's greatest strengths in 2026. With the maturation of structured concurrency libraries and improved tooling for leak detection, writing correct concurrent Go code has never been more accessible—or more important to get right.

Understanding Goroutines in Depth

A goroutine is a lightweight thread managed by the Go runtime. Unlike OS threads, goroutines start with a small stack (around 8KB) that grows and shrinks dynamically. The runtime multiplexes goroutines onto OS threads using an M:N threading model.

package main

import (
    "fmt"
    "sync"
    "time"
)

func worker(id int, wg *sync.WaitGroup) {
    defer wg.Done()
    fmt.Printf("Worker %d starting\n", id)
    time.Sleep(time.Second)
    fmt.Printf("Worker %d done\n", id)
}

func main() {
    var wg sync.WaitGroup
    for i := 1; i <= 5; i++ {
        wg.Add(1)
        go worker(i, &wg)
    }
    wg.Wait()
    fmt.Println("All workers completed")
}

Goroutine Leak Detection

One of the most common mistakes in Go is leaking goroutines—starting goroutines that never terminate. In 2026, the goleak package has become a standard tool for detecting these leaks in tests.

package main_test

import (
    "testing"
    "go.uber.org/goleak"
)

func TestMain(m *testing.M) {
    goleak.VerifyTestMain(m)
}

func TestNoLeak(t *testing.T) {
    defer goleak.VerifyNone(t)
    done := make(chan struct{})
    go func() {
        defer close(done)
    }()
    <-done
}

Common leak patterns to avoid:

// BAD: goroutine blocked on channel send with no receiver
func leakyFunction() {
    ch := make(chan int)
    go func() {
        ch <- 42 // blocks forever if nobody reads
    }()
}

// GOOD: use context for cancellation
func safeFunction(ctx context.Context) {
    ch := make(chan int, 1)
    go func() {
        select {
        case ch <- 42:
        case <-ctx.Done():
            return
        }
    }()
}

Channel Direction and Safety

Specifying channel direction in function signatures enforces correct usage at compile time:

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

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

func main() {
    ch := make(chan int, 10)
    go producer(ch)
    consumer(ch)
}

Select with Timeouts

The select statement is Go's mechanism for multiplexing channel operations. Combining it with timeouts prevents indefinite blocking:

func fetchData(ctx context.Context, url string) (string, error) {
    resultCh := make(chan string, 1)

    go func() {
        time.Sleep(200 * time.Millisecond)
        resultCh <- "data from " + url
    }()

    select {
    case result := <-resultCh:
        return result, nil
    case <-ctx.Done():
        return "", ctx.Err()
    case <-time.After(5 * time.Second):
        return "", fmt.Errorf("request timed out")
    }
}

errgroup: Concurrent Error Handling

The golang.org/x/sync/errgroup package provides structured concurrency with error propagation:

func fetchAll(ctx context.Context, urls []string) ([]string, error) {
    g, ctx := errgroup.WithContext(ctx)
    results := make([]string, len(urls))

    for i, url := range urls {
        i, url := i, url
        g.Go(func() error {
            result, err := fetchURL(ctx, url)
            if err != nil {
                return fmt.Errorf("fetching %s: %w", url, err)
            }
            results[i] = result
            return nil
        })
    }

    if err := g.Wait(); err != nil {
        return nil, err
    }
    return results, nil
}

Limiting concurrency with errgroup:

func fetchAllLimited(ctx context.Context, urls []string, maxConcurrent int) ([]string, error) {
    g, ctx := errgroup.WithContext(ctx)
    g.SetLimit(maxConcurrent)
    results := make([]string, len(urls))
    for i, url := range urls {
        i, url := i, url
        g.Go(func() error {
            result, err := fetchURL(ctx, url)
            if err != nil {
                return err
            }
            results[i] = result
            return nil
        })
    }
    return results, g.Wait()
}

Context Cancellation

Context propagation is essential for clean goroutine management:

func longRunningTask(ctx context.Context, name string) error {
    for {
        select {
        case <-ctx.Done():
            fmt.Printf("Task %s cancelled: %v\n", name, ctx.Err())
            return ctx.Err()
        case <-time.After(500 * time.Millisecond):
            fmt.Printf("Task %s doing work...\n", name)
        }
    }
}

Worker Pool Pattern

Worker pools limit the number of concurrent operations while processing a queue of work:

func workerPool(ctx context.Context, numWorkers int, jobs <-chan Job) <-chan Result {
    results := make(chan Result, numWorkers)
    var wg sync.WaitGroup

    for i := 0; i < numWorkers; i++ {
        wg.Add(1)
        go func(workerID int) {
            defer wg.Done()
            for {
                select {
                case job, ok := <-jobs:
                    if !ok {
                        return
                    }
                    output := fmt.Sprintf("worker %d processed job %d", workerID, job.ID)
                    results <- Result{JobID: job.ID, Output: output}
                case <-ctx.Done():
                    return
                }
            }
        }(i)
    }

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

    return results
}

Fan-Out / Fan-In Patterns

Fan-out distributes work across multiple goroutines; fan-in merges multiple channels into one:

// Fan-out: distribute work to multiple workers
func fanOut(input <-chan int, numWorkers int) []<-chan int {
    channels := make([]<-chan int, numWorkers)
    for i := 0; i < numWorkers; i++ {
        ch := make(chan int)
        channels[i] = ch
        go func(out chan<- int) {
            defer close(out)
            for v := range input {
                out <- v * v
            }
        }(ch)
    }
    return channels
}

// Fan-in: merge multiple channels into one
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 v := range c {
                out <- v
            }
        }(ch)
    }
    go func() {
        wg.Wait()
        close(out)
    }()
    return out
}

Structured Concurrency in 2026

The Go community has embraced structured concurrency principles, ensuring goroutine lifetimes are tied to their parent scope:

type Server struct{ addr string }

func (s *Server) Run(ctx context.Context) error {
    g, ctx := errgroup.WithContext(ctx)

    g.Go(func() error { return s.runHTTPServer(ctx) })
    g.Go(func() error { return s.runMetricsServer(ctx) })
    g.Go(func() error { return s.runBackgroundWorker(ctx) })

    return g.Wait()
}

Best Practices Summary

  1. Always use context: Pass context.Context as the first parameter to any function that starts goroutines.
  2. Buffer channels thoughtfully: Unbuffered channels synchronize; buffered channels decouple.
  3. Use errgroup for parallel work: It handles cancellation, error collection, and waiting automatically.
  4. Detect leaks in tests: Use goleak to catch goroutine leaks before they reach production.
  5. Prefer channel direction types: chan<- T and <-chan T provide compile-time safety.
  6. Use worker pools for I/O: Limit concurrency for network or disk operations to prevent resource exhaustion.

Go's concurrency model in 2026 rewards developers who understand its primitives deeply. By combining goroutines, channels, context, and structured concurrency libraries, you can build systems that are both highly concurrent and reliably correct.