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, 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).
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, which hands back the context the tasks receive alongside it. A Group is used once: after [Wait] has returned, submit to a new one.
[Go] may be called from any goroutine. [Wait] may not run concurrently with [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 (*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.
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/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.