conc: making it hard to leak a goroutine in Go
Better structured concurrency for go
At a glance
- What is it?
- Sourcegraph's structured concurrency library wraps WaitGroup, adds pools, streams and iterators, and stays deliberately small enough to read in one sitting.
- Who is it for?
- conc is worth adopting for one reason above the others: it makes goroutine lifetime a structural property of your code rather than a convention you have to remember. Scoping every goroutine to a WaitGroup that is waited on before it goes out of scope removes an entire class of leak and of panic-crashes-a-day-two-service bug, and it does so with an API small enough to hold in your head.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 52 days ago.
- What is it written in?
- Mainly Go, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Star history
- sourcegraph/conc10,435 ★ · +2
| Repository | 2026-10-07 | 2026-10-23 |
|---|---|---|
| sourcegraph/conc | 10.4k | 10.4k |
2 Stars in the last 16 days, measured from the daily GitHub snapshot. Compare with other projects
Editorial analysis
Three goals, stated in the README without hedging
The package's stated goals are short enough to quote. Make it harder to leak goroutines. Handle panics gracefully. Make concurrent code easier to read. Three goals, no mention of throughput, and that framing is the design.
The first goal is the load-bearing one. The README describes the problem plainly: it is really easy to fire off a go statement and fail to properly wait for it to complete. conc takes what it calls the opinionated stance that all concurrency should be scoped, meaning goroutines should have an owner, and that owner should always ensure its owned goroutines exit properly.
The mechanism is simple. The owner of a goroutine is always a conc.WaitGroup. Goroutines are spawned with `(*WaitGroup).Go()`, and `Wait()` should always be called before the WaitGroup goes out of scope. The idiom that falls out of that rule is a deferred Wait, which is short enough to not need comment:
func main() {
var wg conc.WaitGroup
defer wg.Wait()
startTheThing(&wg)
}
func startTheThing(wg *conc.WaitGroup) {
wg.Go(func() { ... })
}The passing of the WaitGroup into the spawning function covers the case the README calls out separately, where a goroutine needs to outlast the scope of its caller. That pattern is worth internalising, because it is the legitimate escape hatch and having it visible in the API means you do not have to reach for a raw go statement to get it. The README also links out to a well-known essay on why scoped concurrency is worth the discipline in a language without built-in structured concurrency.
Panics: four options, and the library picks three for you
The second goal gets an unusually honest treatment in the README, which enumerates what you can do with a panic once you have caught it: ignore it, log it, turn it into an error and return it to the spawner, or propagate the panic to the spawner.
The README then argues for the options it does not like. Ignoring panics is described as a bad idea since panics usually mean something is actually wrong. Just logging them is also not great, because the spawner gets no indication that something bad happened and might continue on as normal even though the program is in a really bad state. That second observation is the sharp one: a logged-and-swallowed panic in a background goroutine produces a service that reports healthy while quietly having lost work.
So conc implements the two remaining options. Turn the panic into an error and return it, or propagate it to whoever spawned the goroutine. The `panics` package holds this, with a Catcher type, a `Try` function for recovering from a panic raised inside a single function, and a `Recovered` type that can be converted into an error explicitly through `(*Recovered).AsError()`.
That explicit conversion is a deliberate design choice worth noting, and it came from a mistake. In v0.3.0, `RecoveredPanic` was renamed to `Recovered`, and it stopped implementing `error` directly, because it had been producing unintended side effects when something in the standard library took an error interface and behaved differently than expected. Having a recovered panic satisfy `error` implicitly meant it could be passed anywhere an error went, including code paths that inspect error identity or compare against nil in ways the library had not anticipated. Making the conversion explicit costs one call and removes an entire category of surprise.
Pools, streams and iterators, all configured after construction
The library's at-a-glance list is a decision tree rather than a feature list, and it is the fastest way to understand the API. `conc.WaitGroup` is for when you want a safer sync.WaitGroup. `pool.Pool` is a concurrency-limited task runner, `pool.ResultPool` collects task results, `pool.ErrorPool` and `pool.ResultErrorPool` are for fallible tasks, and the ContextPool variants cancel their tasks on failure. `stream.Stream` processes an ordered stream of tasks in parallel while running callbacks serially, `iter.Map` and `iter.ForEach` do concurrent slice operations, and `panics.Catcher` catches panics in your own goroutines.
All pools are created by `pool.New()` or `pool.NewWithResults[T]()` and then configured with methods: `WithMaxGoroutines`, `WithErrors`, `WithContext(ctx)`, `WithFirstError()` and `WithCollectErrored()`. That ordering is worth pausing on, because it is the direct cause of the most useful breaking change in the release history. v0.3.0 made all pool types panic if configured after use. A pool that silently ignored a configuration call made after the first task was submitted would fail in ways that are very hard to diagnose, so the library converts the mistake into an immediate, obvious failure instead.
`stream.Stream` deserves a separate mention because it solves a problem that is awkward to solve directly. When you have an ordered input and need to process items concurrently but hand results to a consumer strictly in order, you normally end up building a pipeline with channels and per-item result buffers. Stream does that with the ordering guarantee built in and callbacks running serially, which removes the most error-prone class of concurrent code from your codebase.
`iter.Map` and `iter.ForEach` are the newest addition and the most convenient for everyday work. They came in v0.2.0 alongside `iter.Iterator` and `iter.Mapper` interfaces that allow configuring the concurrency of ForEach, Map and their relatives, with the top-level functions continuing to work unchanged.
A dependency-free library with one test dependency
The go.mod is worth reading because of what is not in it. The module requires Go 1.20 and exactly one dependency, github.com/stretchr/testify, and testify is the test framework. There are no production dependencies at all.
This is a design decision rather than an accident of a small project. A concurrency library that depends on another concurrency library is a combinatorial problem, and a library that everyone adopts cannot afford to impose a dependency tree on its users. conc's entire surface is built on Go channels, sync and the context package, which is also why it composes with any library that uses those rather than forcing you into a world of its own.
The trade is that you still need to know how those work. conc does not implement structured concurrency, it provides structured programming around Go's existing primitives. Reading the source is a reasonable way to learn it: `waitgroup.go` sits at the repository root as a single file, with the `pool/`, `iter/`, `stream/` and `panics/` subpackages beside it. That is the whole library, and `waitgroup_test.go` at the root suggests the core is tested at the level of the individual file rather than through an integration harness.
The repository also carries a `.golangci.yml` and a Makefile whose default target is help, with lint, test and bench targets beneath it. The test target runs with the race detector enabled, which is the correct choice for a concurrency library and a reasonable signal about how seriously the project treats its own subject matter. The bench target uses a five second benchtime with a zero timeout.
Pre-1.0 means the defaults you read may not be the defaults you get
Three releases, all in early 2023, and the changelogs are unusually candid about being unstable. v0.1.0 shipped on 2023-01-02. v0.2.0 on 2023-01-17, and its notes include an explicit statement that some changes are breaking, that the package is currently pre-1.0, that there are likely to be more breaking changes before a 1.0 release, and that a 1.0 was targeted for March 2023.
No 1.0 appeared. The last release is v0.3.0 from 2023-02-26, yet the repository was last pushed on 2026-09-01 and has 10,430 stars. So the library is in the unusual position of being widely used and still actively developed while carrying a 0.x version number and a public promise of an imminent 1.0 that has not been kept. Whatever the reason, the practical instruction is to pin the version in your go.mod rather than using an unbounded requirement, and to read the changelog when you upgrade.
The v0.2.0 behaviour change is the one most likely to affect working code. `ContextPool` and `ResultContextPool` no longer cancel on the first task that errors by default, and to get that behaviour you must opt in with `WithCancelOnError`. The reasoning is defensible, since cancelling sibling tasks on first error is wrong when your tasks are independent and you want to know about all the failures, but it inverts the behaviour anyone had already built against. v0.3.0 also moved cancellation timing, having the Context pools always cancel their context when `Wait()` returns and also when a task panics.
That same release made a version-dependent choice for multi-error handling, which is the kind of detail worth knowing about if you compare error strings in your tests. On Go 1.20 and later conc uses the standard library multi-error implementation, and on Go 1.19 and earlier it uses the go.uber.org/multierr library instead.
Editorial conclusion
conc is worth adopting for one reason above the others: it makes goroutine lifetime a structural property of your code rather than a convention you have to remember. Scoping every goroutine to a WaitGroup that is waited on before it goes out of scope removes an entire class of leak and of panic-crashes-a-day-two-service bug, and it does so with an API small enough to hold in your head. What it does not do is give you a structured concurrency implementation, since the primitives map onto Go channels, sync and goroutines as they are, which also means it works with any library built on those. Start with WaitGroup.Go and a deferred Wait, read the pool documentation when you need bounded parallelism, and pin the version, because the pre-1.0 releases changed default cancellation behaviour.
Frequently asked questions
What is concurrency in coding?
Concurrency is a program's ability to make progress on several tasks at the same time without requiring them to run at the exact same instant. In Go it is built into the language through goroutines and channels, which is the context conc operates in: it provides structured programming around those primitives rather than replacing them.
How do I install conc?
With go get github.com/sourcegraph/conc. The module declares Go 1.20 and has no production dependencies, only the testify test framework, so adding it pulls in no transitive library tree.
What does conc do with panics in goroutines?
The panics package turns a panic into either a returned error or a value propagated to whoever spawned the goroutine. The README argues against ignoring or merely logging panics, because a logged-and-swallowed panic in a background goroutine lets a program carry on while silently having lost work.
Is conc stable or still pre-1.0?
It is still pre-1.0 at version 0.3.0, released in February 2023, though the repository has been pushed to as recently as 2026-09-01. The v0.2.0 notes said a 1.0 was targeted for March 2023, and it has not appeared. Pin your version, because v0.2.0 changed pool cancellation defaults and v0.3.0 made pools panic when configured after use.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/sourcegraph-conc)Star history chart for your README
[](https://hysenlabs.com/projects/sourcegraph-conc)Was this useful?