conc

conc is a library for structured concurrency in Go that makes common concurrent patterns easier and safer.

It provides tools to manage goroutines, handle panics gracefully, and write more readable concurrent code. This documentation site will guide you through the features and best practices for using conc.

At a Glance

Here is a quick overview of the main components available in conc:

  • conc.WaitGroup: A safer version of sync.WaitGroup that automatically handles panics.
  • pool.Pool: A concurrency-limited task runner for managing a pool of goroutines.
  • pool.ResultPool: A concurrent task runner that collects the results of tasks.
  • pool.ErrorPool: For tasks that can return errors, which are then collected.
  • pool.ContextPool: For tasks that should be canceled together on failure.
  • stream.Stream: Process an ordered stream of tasks in parallel, with callbacks executed serially.
  • iter.Map: Concurrently map a function over a slice.
  • iter.ForEach: Concurrently iterate over a slice.
  • panics.Catcher: A utility to catch panics in your own goroutines.

Core Goals

The main goals of the conc package are:

  1. Make it harder to leak goroutines.
  2. Handle panics gracefully.
  3. Make concurrent code easier to read.

Goal #1: Prevent Goroutine Leaks

A common pain point when working with goroutines is ensuring they are properly cleaned up. It's easy to start a goroutine with go and forget to wait for it to complete.

conc takes the opinionated stance that all concurrency should be scoped. This means goroutines should have a clear owner, and that owner is responsible for ensuring the goroutines exit properly. In conc, the owner is typically a conc.WaitGroup or one of the pool types. You spawn goroutines using methods like wg.Go() or pool.Go(), and you must always call Wait() to ensure completion.

This approach is inspired by the principles of structured concurrency, as detailed in posts like Notes on structured concurrency, or: go statement considered harmful.

Goal #2: Graceful Panic Handling

A goroutine that panics without a recovery mechanism will crash the entire application. While you can add a recover() block, deciding what to do with the recovered panic is not always straightforward.

conc ensures that panics are always handled. When a goroutine managed by conc panics, the panic is caught and propagated to the caller of Wait(). The panic value is decorated with a stack trace from the child goroutine, preserving crucial debugging information.

This built-in handling removes boilerplate and makes your concurrent code more robust.

Standard Library vs. conc Panic Handling

// stdlib: requires significant boilerplate
type caughtPanicError struct {
    val   any
    stack []byte
}

func (e *caughtPanicError) Error() string {
    return fmt.Sprintf(
        "panic: %q\n%s",
        e.val,
        string(e.stack),
    )
}

func main() {
    done := make(chan error)
    go func() {
        defer func() {
            if v := recover(); v != nil {
                done <- &caughtPanicError{
                    val: v,
                    stack: debug.Stack(),
                }
            } else {
                done <- nil
            }
        }()
        doSomethingThatMightPanic()
    }()
    err := <-done
    if err != nil {
        panic(err)
    }
}
// conc: clean and automatic
func main() {
    var wg conc.WaitGroup
    wg.Go(doSomethingThatMightPanic)
    // panics with a nice stacktrace
    wg.Wait()
}

Goal #3: Improve Readability

Correct and readable concurrent code is difficult to write. conc aims to simplify common patterns by abstracting away the boilerplate complexity.

Whether you need to run tasks with a limited number of goroutines, process a stream in order, or map over a slice concurrently, conc provides a clean, high-level API to accomplish the task.