Repository: go
Issue: proposal: sync: add golang.org/x/sync/errgroup
Labels: Proposal

<!--
Our proposal process is documented here:
https://go.dev/s/proposal-process
-->


As briefly discussed in https://github.com/golang/go/issues/56102#issuecomment-1317765390, I propose to promote `errgroup.Group` to the `sync` package. The proposed API set is listed below.

## Rationale

Compared to `sync.WaitGroup`, `errgroup.Group` do not require cognitive load to manage `Add()` and `Done()` and can easily manage the number of concurrent tasks using `SetLimit`. For example,

```
g := sync.WaitGroup{}
sem := make(chan struct{}, 5)
for i := 0; i < n; i++ {
	sem <- struct{}{}
	g.Add(1)
	go func() {
		defer func() {
			g.Done()
			<-sem
		}()

		// ...
	}()
}
g.Wait()
```

vs.

```
g := errgroup.Group{}
g.SetLimit(5)
for i := 0; i < n; i++ {
	g.Go(func() {
		// ...
	})
}
g.Wait()
```

Tu et al. [1] also reported that `WaitGroup` is often misused and causes concurrency bugs. For instance, an example taken from Figure 9:

```diff
func (p *peer) send() {
	p.mu.Lock()
	defer p.mu.Unlock()
	switch p.status {
		case idle:
+			p.wg.Add(1)
			go func() {
-				p.wg.Add(1)
				...
				p.wg.Done()
			}()
		case stopped:
	}
}

func (p * peer) stop() {
	p.mu.Lock()
	p.status = stopped
	p.mu.Unlock()
	p.wg.Wait()
}
```

[1] Tu, Tengfei, et al. "Understanding real-world concurrency bugs in go." Proceedings of the Twenty-Fourth International Conference on Architectural Support for Programming Languages and Operating Systems. 2019. https://doi.org/10.1145/3297858.3304069

## Existing Usage

A search over GitHub, code that uses `errgroup.Group` includes [16.1k files](https://github.com/search?q=errgroup.Group+lang%3AGo+&type=code) and pkg.go.dev shows there are [10,456](https://pkg.go.dev/golang.org/x/sync@v0.1.0/errgroup?tab=importedby) imports.

## APIs

```
package sync

// An ErrGroup is a collection of goroutines working on subtasks that are part of
// the same overall task.
//
// A zero ErrGroup is valid, has no limit on the number of active goroutines,
// and does not cancel on error.
type ErrGroup struct

// SetLimit limits the number of active goroutines in this group to at most n.
// A negative value indicates no limit.
//
// Any subsequent call to the Go method will block until it can add an active
// goroutine without exceeding the configured limit.
//
// The limit must not be modified while any goroutines in the group are active.
func (g *ErrGroup) SetLimit(n int)

// Go calls the given function in a new goroutine.
// It blocks until the new goroutine can be added without the number of
// active goroutines in the group exceeding the configured limit.
//
// The first call to return a non-nil error cancels the group's context, if the
// group was created by calling WithContext. The error will be returned by Wait.
func (g *ErrGroup) Go(f func() error)

// TryGo calls the given function in a new goroutine only if the number of
// active goroutines in the group is currently below the configured limit.
//
// The return value reports whether the goroutine was started.
func (g *ErrGroup) TryGo(f func() error) bool

// Wait blocks until all function calls from the Go method have returned, then
// returns the first non-nil error (if any) from them.
func (g *ErrGroup) Wait() error

// NewErrGroupWithContext returns a new ErrGroup and an associated Context derived from ctx.
//
// The derived Context is canceled the first time a function passed to Go
// returns a non-nil error or the first time Wait returns, whichever occurs
// first.
func NewErrGroupWithContext(ctx context.Context) (*ErrGroup, context.Context)
```

Update: `WithContext` is renamed to `NewErrGroupWithContext`.