conc

repository·main·Indexed 27 days ago

https://github.com/sourcegraph/conc

A toolkit for structured concurrency in Go designed to make concurrent code safer and prevent goroutine leaks. It provides abstractions such as conc.WaitGroup for panic-handling scoped concurrency, pool.Pool for concurrency-limited task running, stream.Stream for ordered parallel processing, and iter utilities for concurrent mapping and iteration of slices.

Tokens
1.7K
Snippets
8
Records
15
Agent score
45%

What's inside conc

  1. Overview of conc components

    main

    The conc package provides several tools for structured concurrency in Go:

    • conc.WaitGroup: A safer version of sync.WaitGroup that handles panics.
    • pool.Pool: A concurrency-limited task runner.
    • pool.ResultPool: A concurrent task runner that collects task results.
    • pool.ErrorPool: A concurrent task runner for fallible tasks.
    • pool.ContextPool: A concurrent task runner that cancels tasks on failure.
    • stream.Stream: Processes an ordered stream of tasks in parallel with serial callbacks.
    • iter.Map: Concurrently maps a slice.
    • iter.ForEach: Concurrently iterates over a slice.
    • panics.Catcher: Catches panics in your own goroutines.
  2. Handle panics gracefully with conc.WaitGroup

    main

    Unlike standard goroutines which crash the entire process on panic, conc.WaitGroup catches panics. When wg.Wait() is called, if any spawned goroutine panicked, Wait() will panic itself, decorating the value with a stacktrace from the child goroutine to preserve debugging information.

    func main() {
        var wg conc.WaitGroup
        wg.Go(doSomethingThatMightPanic)
        // panics with a nice stacktrace
        wg.Wait()
    }
  3. Use conc.WaitGroup for scoped concurrency

    main

    To prevent goroutine leaks, conc encourages scoped concurrency where every goroutine has an owner. Use conc.WaitGroup to spawn goroutines with .Go() and ensure .Wait() is called before the WaitGroup goes out of scope. If you need a goroutine to outlive the caller, pass the *conc.WaitGroup into the spawning function.

    func main() {
        var wg conc.WaitGroup
        defer wg.Wait()
    
        startTheThing(&wg)
    }
    
    func startTheThing(wg *conc.WaitGroup) {
        wg.Go(func() { ... })
    }
  4. Spawn and wait for goroutines with conc.WaitGroup

    main

    Use conc.WaitGroup as a drop-in, safer replacement for sync.WaitGroup. It simplifies the syntax by allowing you to pass a function directly to .Go(), which handles the boilerplate of adding to the group and signaling completion.

    func main() {
        var wg conc.WaitGroup
        for i := 0; i < 10; i++ {
            wg.Go(doSomething)
        }
        wg.Wait()
    }
  5. Process a stream in a static pool with pool.New()

    main

    To process elements from a channel (stream) using a fixed number of goroutines, use pool.New() combined with .WithMaxGoroutines(n). This prevents unbounded goroutine creation.

    func process(stream chan int) {
        p := pool.New().WithMaxGoroutines(10)
        for elem := range stream {
            elem := elem
            p.Go(func() {
                handle(elem)
            })
        }
        p.Wait()
    }
  6. Process an ordered stream concurrently with stream.New()

    main

    To process elements from an input channel and ensure they are sent to an output channel in the same order they were received, use stream.New(). You can limit concurrency using .WithMaxGoroutines(n).

    Each task passed to .Go() must return a stream.Callback (a function) that performs the actual output operation (e.g., sending to a channel).

    func mapStream(
        in chan int,
        out chan int,
        f func(int) int,
    ) {
        s := stream.New().WithMaxGoroutines(10)
        for elem := range in {
            elem := elem
            s.Go(func() stream.Callback {
                res := f(elem)
                return func() { out <- res }
            })
        }
        s.Wait()
    }
  7. Concurrently map a slice with iter.Map

    main

    Use iter.Map to transform a slice into a new slice concurrently. The function passed to iter.Map should accept a pointer to the element (e.g., func(*int) int).

    func concMap(
        input []int,
        f func(*int) int,
    ) []int {
        return iter.Map(input, f)
    }
  8. Iterate over a slice concurrently with iter.ForEach

    main

    Instead of manually managing workers and channels to process a slice, use iter.ForEach to execute a function on every element of a slice concurrently.

    func process(values []int) {
        iter.ForEach(values, handle)
    }
  9. Configure conc pools

    main

    All pools are created using pool.New() or pool.NewWithResults[T](), and can be configured with the following methods:

    • p.WithMaxGoroutines(): Configures the maximum number of goroutines in the pool.
    • p.WithErrors(): Configures the pool to run tasks that return errors.
    • p.WithContext(ctx): Configures the pool to run tasks that should be canceled on first error.
    • p.WithFirstError(): (For ErrorPool) Configures the pool to only keep the first returned error rather than an aggregated error.
    • p.WithCollectErrored(): (For ResultContextPool) Configures result pools to collect results even when the task errored.
  10. Use WaitGroup for structured concurrency

    main
    The WaitGroup type is the primary building block for scoped concurrency in conc. It allows you to spawn goroutines using the Go method and ensures that all spawned goroutines exit before Wait() returns. Unlike sync.WaitGroup, conc.WaitGroup automatically catches panics in child goroutines and propagates them to the caller.
  11. Initialize a WaitGroup with NewWaitGroup

    main
    Use NewWaitGroup() to create a new instance of WaitGroup. While the zero value of WaitGroup is usable (similar to sync.WaitGroup), using the constructor is the standard way to ensure all internal components, like the panic catcher, are properly initialized.