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 ofsync.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:
- Make it harder to leak goroutines.
- Handle panics gracefully.
- 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.