What do errgroup.Group.SetLimit and TryGo do, and when does SetLimit panic?
answer
- the default is no limit at all
- who blocks when the group is full
- the non-blocking variant returns a bool
- set it once, before any task starts
- changing it under running tasks panics
basics
~20 serrgroup.Group.SetLimit(n) caps the group at n active tasks, so Group.Go blocks until a slot frees; a negative n means no limit. TryGo starts a task only if there is room, returning false otherwise. SetLimit panics while tasks are active.
solid answer
~40 sBy default an `errgroup.Group` starts every task you hand it, so a loop over ten thousand inputs launches ten thousand goroutines. `g.SetLimit(n)` bounds that: at most `n` tasks are active at once, and `g.Go` blocks the *calling* goroutine until a slot frees, which also throttles the loop that is feeding it. Passing a negative value removes the limit. `g.TryGo(f)` is the non-blocking variant — it starts `f` only if the group is currently under the limit and returns `true`, otherwise it returns `false` and does not run `f`, leaving you to shed, queue or run the work inline. The limit must be set before you start any tasks: calling `SetLimit` while goroutines from that group are still active panics, so treat it as configuration applied once, right after the group is created.
go deeper
Know that a group without SetLimit starts one goroutine per task, and that SetLimit(n) caps that at n. Remember TryGo returns a bool saying whether the task was started.
Explain that Group.Go blocks the caller, so the limit throttles the producing loop itself, and that a negative limit means unlimited while changing the limit under active tasks panics.
Justify the number from the downstream's capacity rather than the machine's, keep it in configuration, and know that a blocked Group.Go ignores context cancellation so a big loop needs its own check.
Decide who owns the limit across services that share one database or upstream, and whether a per-process cap is even the right control when many replicas each apply it independently.
## Why a limit is needed at all An `errgroup.Group` with no limit is a faithful `go` statement per task. That is fine for a fan-out of four backends and disastrous for a loop over a large input, where it becomes an unbounded goroutine count and, far more expensively, an unbounded number of simultaneous requests aimed at whatever those tasks call. The failure is rarely the goroutines themselves — they are cheap — it is the connection pool, the upstream rate limit or the memory held by every in-flight buffer at once. ## SetLimit ``` g, ctx := errgroup.WithContext(ctx) g.SetLimit(8) for _, item := range items { g.Go(func() error { return process(ctx, item) }) } err := g.Wait() ``` `SetLimit(8)` means at most eight functions passed to `Go` are running at any moment. The important detail is *where the waiting happens*: `Go` blocks the goroutine that calls it. In the loop above, that is the loop itself, so the producer is throttled by the consumers — you never materialise ten thousand pending closures. That property is what makes `SetLimit` a real backpressure mechanism rather than a queue depth. Two further facts: - **A negative limit means no limit**, which is also the state of a fresh group. - **A limit of zero means `Go` can never start anything** and will block forever. It is not a useful setting; if you compute the limit at runtime, guard against zero. ## The panic `SetLimit` must not be called while any goroutine in the group is still active. Doing so panics, deliberately and immediately, rather than silently reinterpreting the semaphore under running tasks. In practice this means: create the group, set the limit, then start work. Trying to "turn the limit up when the queue looks long" is not something this API supports — if you need a dynamic limit, you need a different mechanism or a second group. ## TryGo `TryGo(f) bool` is `Go` without the blocking. If the group is below its limit, it starts `f` and returns `true`; if not, it returns `false` and `f` is never run. With no limit configured, `TryGo` always starts the function and returns `true`. That boolean is the whole point: it hands the decision back to you. Typical uses are shedding — a background refresh that is worth doing only if capacity is free, and skippable otherwise — or a producer that would rather do the work inline on its own goroutine than block. It is not a substitute for `Go` in a plain fan-out; if every task must run, blocking in `Go` is what you want, and ignoring `TryGo`'s return value is a bug the compiler will not catch for you. ## Choosing the number The limit is a statement about the *downstream*, not about the machine. For I/O-bound tasks it should be derived from what the thing you are calling can absorb — the database connection pool size, the upstream's rate limit, the point where p99 latency starts climbing — and it belongs in configuration where it can be changed without a release. For CPU-bound tasks a value tied to `runtime.NumCPU()` is defensible, though the Go scheduler already multiplexes goroutines onto that many threads, so the limit there is mostly about memory held per in-flight task. ## Interaction with cancellation `Go` blocking for a slot is unconditional: it waits for a slot, not for the group's context. After a sibling fails and the derived context is cancelled, a loop still sitting in `g.Go` continues to launch the remaining tasks as slots free up. Those tasks should see the cancelled context and return quickly, but they are started. If a large input loop needs to stop feeding the group promptly, check `ctx.Err()` at the top of the loop body and `break` — the group will not do it for you. ## Summary `SetLimit` is set-once configuration that turns the group into a bounded worker set and throttles the producer through a blocking `Go`. `TryGo` is the non-blocking probe for work you are willing to skip. Change the limit while tasks run and you get a panic, which is the API telling you that the limit is a construction-time decision.
- With errgroup.Group.SetLimit(8) and a loop over 10,000 items, how many goroutines exist at once?At most eight from the group, plus the loop's own goroutine. Because `Go` blocks when the group is full, the loop is throttled at the source — the remaining items stay as loop iterations that have not happened yet, not as pending goroutines or queued closures. That is the difference between a real concurrency limit and a work queue that merely buffers the backlog somewhere else.
- The group's context is cancelled while a loop is still calling errgroup.Group.Go. Does the loop stop?No. `Go` blocks for a free slot only; it does not observe the group's context, so the loop keeps launching the remaining tasks as slots free. Each launched task should see the cancelled context and return almost immediately, but you still pay for launching them. If that matters, test `ctx.Err() != nil` at the top of the loop body and break out yourself.
- When would you use TryGo rather than Go?When the work is optional. A cache warm-up, a speculative prefetch or a best-effort background refresh is worth doing if capacity happens to be free and worth skipping otherwise — `TryGo` returning `false` is your shed signal. For work that must happen, use `Go` and let it block; silently dropping a mandatory task because you ignored `TryGo`'s boolean is a bug nothing will flag.
saying these in an interview costs you the question
- Thinking Group.Go returns an error when the group is full
- Calling SetLimit after tasks have already started
- Ignoring the boolean that TryGo returns
- Assuming a limit of zero means unlimited
- Expecting a blocked Group.Go to abort on context cancellation