What is quite sad is that we cannot add it ourselves as it's so simple of what they have done:
func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}What is quite sad is that we cannot add it ourselves as it's so simple of what they have done:
func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}errgroup also has other niceties like error propagation, context cancellation, and concurrency limiting.
At best, using the optional, higher-effort errgroup.WithContext will cancel the context but still run all of your funcs. If you don't want that for one of the funcs, or some component of them, just don't use the context.
You could also just make your subtask function return nil always, if you just want to get the automatic bookkeeping call pattern (like WaitGroup.Go from Golang 1.25), plus optional concurrency limiting.
Also note, even if a subtask function returns an error, the errgroup Wait blocking semantics are identical to those of a WaitGroup. Wait will return the first error when it returns, but it doesn't unblock early on first error.
My recommendation would be to have a NewGroup function or equivalent that returns an empty group to surface it as an alternative to WithContext.
That goes against common practices in Golang, articulated in the second paragraph of https://go.dev/doc/effective_go#allocation_new among many other places.
Also the errgroup documentation specifically says "A zero Group is valid, has no limit on the number of active goroutines, and does not cancel on error." And, as you noted, one of the examples doesn't use WithContext.
package main
import (
"sync"
"time"
)
type WaitGroup struct {
sync.WaitGroup
}
func (wg *WaitGroup) Go(fn func()) {
wg.Add(1)
go func() {
defer wg.Done()
fn()
}()
}
func main() {
var wg WaitGroup
wg.Go(func() { time.Sleep(1 * time.Second) })
wg.Wait()
}