Documentation
¶
Overview ¶
Package group — the typed fan-out built on the Group.
Package group runs a set of tasks concurrently and gives their caller one place to wait: one first error — or, from NewJoined, every error — one bounded degree of parallelism, and — the part the familiar shape leaves out — the panic that a child goroutine would otherwise take the whole process down with. It is a kernel primitive (stdlib-only, fully domain-neutral: Group, Go, Wait, Collect; no Job, no Task, no Worker appears in a signature).
One error, or every error ¶
A group from New reports the FIRST error, because almost every error after it is a consequence of the cancellation the first one caused. A group from NewJoined reports EVERY task's error, joined with errors.Join in submission order — for tasks whose failures are independent facts, such as workers polling one medium, where the second failure is not an echo of the first. Both cancel the siblings on the first failure, and in both the first failure is the context's cause.
A panic in a child is delivered, not fatal ¶
A goroutine that panics and is not recovered ON ITS OWN STACK kills the process. No amount of recover() in the parent helps, which is why the common "spawn N goroutines, collect errors" helper turns one bad input into a crash with a stack pointing at a goroutine the reader has never seen. Here the recovery happens inside the child, together with the stack of the goroutine that actually failed, and is re-raised in Group.Wait as a PanicValue.
The re-raise happens AFTER every other task has returned, so the structured guarantee survives the fault: when Wait leaves — by return or by panic — the group owns no running goroutine.
What Wait guarantees, and what it cannot ¶
Wait returns when every task started by Group.Go has RETURNED. It does not return when the context is cancelled, because cancellation is a request and Go has no way to force a goroutine to honour one.
So a task that ignores its context keeps Wait blocked for as long as it runs, and that is the contract rather than a defect: the alternative — returning while goroutines are still live — is precisely the leak structured concurrency exists to prevent, and it would hand the caller a "finished" group that is still writing to their memory. Bound such a task from the inside (a deadline on the I/O it performs). A group cannot bound it from the outside without abandoning it.
Limits ¶
The concurrency limit is a real bound taken before the goroutine is created, so N submissions against a limit of L hold L goroutines, not N. A non-positive limit is clamped to 1 and Unlimited must be spelled out — see New.
Package group — the panic carried off a task's goroutine and re-raised in the waiter.
Index ¶
Constants ¶
const Unlimited int = math.MaxInt
Unlimited is the concurrency limit that bounds nothing: a slot is always free, so Group.Go never waits for one.
It has to be written out. A zero that silently meant "no limit" would be a value the caller never chose doing something they never asked for, and a zero that meant "no task may run" is the deadlock ADR 0031 exists to remove from this SDK — errgroup's SetLimit(0) parks every Go call forever. Naming the case leaves neither reading available to a typo.
Variables ¶
This section is empty.
Functions ¶
func Collect ¶
func Collect[T any](parent context.Context, limit int, fns []func(ctx context.Context) (value T, err error)) (results []T, err error)
Collect runs every function in fns concurrently under a Group and returns their results in SUBMISSION order: index i of the result is fns[i]'s value, whatever order the tasks actually finished in.
limit is New's, with the same clamp and the same Unlimited spelling.
It is a function rather than a method because Go's methods take no type parameters of their own: a Group that both bounded concurrency and carried a result type would have to fix that type at construction, forcing Group[struct{}] on every caller who only wants to wait. Keeping the result type here leaves the common shape untyped and this one exact.
On the first error the remaining tasks see a cancelled context and Collect returns (nil, err). No partial slice is handed back: a half-filled result is indistinguishable at the call site from a complete one, and the zero values in it would read as answers.
A panic in any fn propagates out of Collect exactly as it does out of Group.Wait — after every other task has returned, carrying the failing goroutine's stack.
Types ¶
type Group ¶
type Group struct {
// contains filtered or unexported fields
}
Group runs tasks concurrently under one context and one wait point.
The zero value is NOT usable — construct with New or NewJoined, which hand back the context the tasks receive alongside it. A Group is used once: after Group.Wait has returned, submit to a new one.
Group.Go may be called from any goroutine. Group.Wait may not run concurrently with Group.Go — "have all submissions been made" is a question only the submitter can answer, and a Group that guessed would report a total that was true for an instant.
func New ¶
New returns a Group and the context every task submitted to it receives.
limit bounds how many tasks run at once. A non-positive limit is CLAMPED to 1 — one task at a time, slow but correct — and is read neither as "no limit" nor as "no task may run"; pass Unlimited for the former, and note that the latter is what makes a zero limit a deadlock elsewhere. The clamp follows the SDK's own bulkhead precedent (ADR 0031 §"Why not extend this to the other clamps"): a concurrency floor of one still runs the caller's work under a meaningful, if minimal, policy, so it is a floor rather than a guess at intent.
The returned context is cancelled when the first task fails, when a task panics, or when Group.Wait returns — whichever happens first. On a task failure that task's error is the context's context.Cause.
func NewJoined ¶
NewJoined returns a Group whose Group.Wait reports EVERY task's error, joined with errors.Join in the order the tasks were submitted, rather than the first one. Everything else is New's: the limit and its clamp, the returned context, the siblings cancelled on the first failure — whose error is still the context's context.Cause — and a panic re-raised in Wait, which still outranks every error.
It is for tasks whose failures are independent facts: N workers polling one broken medium each report their own failure, and dropping all but one would hide which of them saw what. When later failures are mostly the echo of the cancellation the first one caused, New is the right group.
The joined error is matchable as each of its parts — errors.Is, errors.As, and the SDK's errs.HasCode all walk Unwrap() []error — and Wait returns nil, not an empty join, when no task failed.
func (*Group) Go ¶
Go starts fn on a goroutine of its own, blocking while the group already has its limit of tasks running.
fn receives the group's context. A task submitted after the group has already failed still RUNS, with an already-cancelled context that a cooperative task returns from at once. Skipping it silently would be the worse half of the trade — a task that never ran and never said so.
Go must not be called after Group.Wait.
func (*Group) Wait ¶
Wait blocks until every task started by Group.Go has returned, releases the group context, and returns the first error any task reported — or, for a group from NewJoined, every one of them, joined in submission order.
If a task panicked, Wait re-raises it as a PanicValue carrying the stack of the goroutine that failed — after every other task has finished, so no task outlives Wait on the panic path either. A panic outranks an error: an error is an outcome the caller can handle, a panic says the result is not one.
Wait does NOT return early on cancellation. See the package documentation for what that guarantees and what it costs.
type PanicValue ¶
type PanicValue struct {
// Raised is the value the original panic carried, verbatim.
Raised any
// Stack is the stack of the goroutine that ran the task, captured at
// recovery.
Stack []byte
}
PanicValue carries a panic raised inside a task across the goroutine boundary to whoever is waiting on the group.
It is deliberately NOT an error. A panic is a programming fault, and turning one into a value the caller may ignore is how a broken invariant becomes a silent wrong answer — the same position kernel/concur/singleflight takes, and the reason neither type carries an error code.
The stack is captured at the moment of recovery, inside the goroutine that ran the task. Without it a re-raised panic would point at the waiter's stack — code that did nothing wrong — and the frame that actually failed would be gone, which is the failure mode that makes a crashed worker pool so expensive to debug.
It is a separate type from singleflight's namesake rather than a shared one: each names its own package in the message a crash prints, and hoisting the two into a fourth kernel package would add a primitive whose entire content is two fields.
func (PanicValue) String ¶
func (p PanicValue) String() string
String renders the original panic followed by the stack of the goroutine that raised it. The Go runtime prints a panic value through String when the value has one, so an uncaught re-raise shows the task's stack rather than only the waiter's.