ErrGroup: Go's Hidden Gem for Concurrent Programming
James Reed
Infrastructure Engineer · Leapcell

Go Language errgroup Library: A Powerful Concurrency Control Tool
errgroup
is a utility in the official Go library x
used for concurrently executing multiple goroutines
and handling errors. It implements errgroup.Group
based on sync.WaitGroup
, providing more powerful functions for concurrent programming.
Advantages of errgroup
Compared with sync.WaitGroup
, errgroup.Group
has the following advantages:
- Error Handling:
sync.WaitGroup
is only responsible for waiting for thegoroutines
to complete and does not handle return values or errors. Whileerrgroup.Group
cannot directly handle return values, it can immediately cancel other runninggoroutines
when agoroutine
encounters an error and return the first non-nil
error in theWait
method. - Context Cancellation:
errgroup
can be used in conjunction withcontext.Context
. When agoroutine
encounters an error, it can automatically cancel othergoroutines
, effectively controlling resources and avoiding unnecessary work. - Simplifying Concurrent Programming: Using
errgroup
can reduce the boilerplate code for error handling. Developers do not need to manually manage error states and synchronization logic, making concurrent programming simpler and more maintainable. - Limiting the Number of Concurrency:
errgroup
provides an interface to limit the number of concurrentgoroutines
to avoid overloading, which is a feature thatsync.WaitGroup
does not have.
Example of Using sync.WaitGroup
Before introducing errgroup.Group
, let's first review the usage of sync.WaitGroup
.
package main import ( "fmt" "net/http" "sync" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } var err error var wg sync.WaitGroup for _, url := range urls { wg.Add(1) go func() { defer wg.Done() resp, e := http.Get(url) if e != nil { err = e return } defer resp.Body.Close() fmt.Printf("fetch url %s status %s\n", url, resp.Status) }() } wg.Wait() if err != nil { fmt.Printf("Error: %s\n", err) } }
Execution result:
$ go run waitgroup/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
Typical idiom of sync.WaitGroup
:
var wg sync.WaitGroup for ... { wg.Add(1) go func() { defer wg.Done() // do something }() } wg.Wait()
Example of Using errgroup.Group
Basic Usage
The usage pattern of errgroup.Group
is similar to that of sync.WaitGroup
.
package main import ( "fmt" "net/http" "golang.org/x/sync/errgroup" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } var g errgroup.Group for _, url := range urls { g.Go(func() error { resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() fmt.Printf("fetch url %s status %s\n", url, resp.Status) return nil }) } if err := g.Wait(); err != nil { fmt.Printf("Error: %s\n", err) } }
Execution result:
$ go run examples/main.go
fetch url http://www.google.com/ status 200 OK
fetch url http://www.golang.org/ status 200 OK
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
Context Cancellation
errgroup
provides errgroup.WithContext
to add a cancellation function.
package main import ( "context" "fmt" "net/http" "sync" "golang.org/x/sync/errgroup" ) func main() { var urls = []string{ "http://www.golang.org/", "http://www.google.com/", "http://www.somestupidname.com/", } g, ctx := errgroup.WithContext(context.Background()) var result sync.Map for _, url := range urls { g.Go(func() error { req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return err } resp, err := http.DefaultClient.Do(req) if err != nil { return err } defer resp.Body.Close() result.Store(url, resp.Status) return nil }) } if err := g.Wait(); err != nil { fmt.Println("Error: ", err) } result.Range(func(key, value any) bool { fmt.Printf("fetch url %s status %s\n", key, value) return true }) }
Execution result:
$ go run examples/withcontext/main.go
Error: Get "http://www.somestupidname.com/": dial tcp: lookup www.somestupidname.com: no such host
fetch url http://www.google.com/ status 200 OK
Since the request to http://www.somestupidname.com/ reported an error, the program cancelled the request to http://www.golang.org/.
Limiting the Number of Concurrency
errgroup
provides errgroup.SetLimit
to limit the number of concurrently executing goroutines
.
package main import ( "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group g.SetLimit(3) for i := 1; i <= 10; i++ { g.Go(func() error { fmt.Printf("Goroutine %d is starting\n", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d is done\n", i) return nil }) } if err := g.Wait(); err != nil { fmt.Printf("Encountered an error: %v\n", err) } fmt.Println("All goroutines complete.") }
Execution result:
$ go run examples/setlimit/main.go
Goroutine 3 is starting
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 is done
Goroutine 1 is done
Goroutine 5 is starting
Goroutine 3 is done
Goroutine 6 is starting
Goroutine 4 is starting
Goroutine 6 is done
Goroutine 5 is done
Goroutine 8 is starting
Goroutine 4 is done
Goroutine 7 is starting
Goroutine 9 is starting
Goroutine 9 is done
Goroutine 8 is done
Goroutine 10 is starting
Goroutine 7 is done
Goroutine 10 is done
All goroutines complete.
Try to Start
errgroup
provides errgroup.TryGo
to try to start a task, which needs to be used in conjunction with errgroup.SetLimit
.
package main import ( "fmt" "time" "golang.org/x/sync/errgroup" ) func main() { var g errgroup.Group g.SetLimit(3) for i := 1; i <= 10; i++ { if g.TryGo(func() error { fmt.Printf("Goroutine %d is starting\n", i) time.Sleep(2 * time.Second) fmt.Printf("Goroutine %d is done\n", i) return nil }) { fmt.Printf("Goroutine %d started successfully\n", i) } else { fmt.Printf("Goroutine %d could not start (limit reached)\n", i) } } if err := g.Wait(); err != nil { fmt.Printf("Encountered an error: %v\n", err) } fmt.Println("All goroutines complete.") }
Execution result:
$ go run examples/trygo/main.go
Goroutine 1 started successfully
Goroutine 1 is starting
Goroutine 2 is starting
Goroutine 2 started successfully
Goroutine 3 started successfully
Goroutine 4 could not start (limit reached)
Goroutine 5 could not start (limit reached)
Goroutine 6 could not start (limit reached)
Goroutine 7 could not start (limit reached)
Goroutine 8 could not start (limit reached)
Goroutine 9 could not start (limit reached)
Goroutine 10 could not start (limit reached)
Goroutine 3 is starting
Goroutine 2 is done
Goroutine 3 is done
Goroutine 1 is done
All goroutines complete.
Source Code Interpretation
The source code of errgroup
mainly consists of 3 files:
- Main logic code
- Implementation of withCancelCause for Go 1.24 and higher versions
- Implementation of withCancelCause for versions lower than Go 1.24
Core Structure
type token struct{} type Group struct { cancel func(error) wg sync.WaitGroup sem chan token errOnce sync.Once err error }
token
: An empty structure used to pass signals to control the number of concurrency.Group
:cancel
: The function called when the context is cancelled.wg
: The internally usedsync.WaitGroup
.sem
: The signal channel that controls the number of concurrent coroutines.errOnce
: Ensures that the error is handled only once.err
: Records the first error.
Main Methods
- SetLimit: Limits the number of concurrency.
func (g *Group) SetLimit(n int) { if n < 0 { g.sem = nil return } if len(g.sem) != 0 { panic(fmt.Errorf("errgroup: modify limit while %v goroutines in the group are still active", len(g.sem))) } g.sem = make(chan token, n) }
- Go: Starts a new coroutine to execute the task.
func (g *Group) Go(f func() error) { if g.sem != nil { g.sem <- token{} } g.wg.Add(1) go func() { defer g.done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel(g.err) } }) } }() }
- Wait: Waits for all tasks to complete and returns the first error.
func (g *Group) Wait() error { g.wg.Wait() if g.cancel != nil { g.cancel(g.err) } return g.err }
- TryGo: Tries to start a task.
func (g *Group) TryGo(f func() error) bool { if g.sem != nil { select { case g.sem <- token{}: default: return false } } g.wg.Add(1) go func() { defer g.done() if err := f(); err != nil { g.errOnce.Do(func() { g.err = err if g.cancel != nil { g.cancel(g.err) } }) } }() return true }
Conclusion
errgroup
is an official extended library that adds error handling capabilities on the basis of sync.WaitGroup
, providing functions such as synchronization, error propagation, and context cancellation. Its WithContext
method can add a cancellation function, SetLimit
can limit the number of concurrency, and TryGo
can try to start a task. The source code is ingeniously designed and worthy of reference.
Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis
Finally, I would like to recommend the most suitable platform for deploying golang: Leapcell
1. Multi-Language Support
- Develop with JavaScript, Python, Go, or Rust.
2. Deploy unlimited projects for free
- pay only for usage — no requests, no charges.
3. Unbeatable Cost Efficiency
- Pay-as-you-go with no idle charges.
- Example: $25 supports 6.94M requests at a 60ms average response time.
4. Streamlined Developer Experience
- Intuitive UI for effortless setup.
- Fully automated CI/CD pipelines and GitOps integration.
- Real-time metrics and logging for actionable insights.
5. Effortless Scalability and High Performance
- Auto-scaling to handle high concurrency with ease.
- Zero operational overhead — just focus on building.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ