DEV Community

Cover image for ErrGroup: Unlocking Go's Concurrency Power
Leapcell
Leapcell

Posted on

ErrGroup: Unlocking Go's Concurrency Power

Image description

Leapcell: The Next-Gen Serverless Platform for Web Hosting, Async Tasks, and Redis

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:

  1. Error Handling: sync.WaitGroup is only responsible for waiting for the goroutines to complete and does not handle return values or errors. While errgroup.Group cannot directly handle return values, it can immediately cancel other running goroutines when a goroutine encounters an error and return the first non-nil error in the Wait method.
  2. Context Cancellation: errgroup can be used in conjunction with context.Context. When a goroutine encounters an error, it can automatically cancel other goroutines, effectively controlling resources and avoiding unnecessary work.
  3. 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.
  4. Limiting the Number of Concurrency: errgroup provides an interface to limit the number of concurrent goroutines to avoid overloading, which is a feature that sync.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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Typical idiom of sync.WaitGroup:

var wg sync.WaitGroup

for ... {
    wg.Add(1)

    go func() {
        defer wg.Done()
        // do something
    }()
}

wg.Wait()
Enter fullscreen mode Exit fullscreen mode

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)
    }
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
    })
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.")
}
Enter fullscreen mode Exit fullscreen mode

Execution result:

$  go run examples/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.
Enter fullscreen mode Exit fullscreen mode

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.")
}
Enter fullscreen mode Exit fullscreen mode

Execution result:

$ go run examples/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.
Enter fullscreen mode Exit fullscreen mode

Source Code Interpretation

The source code of errgroup mainly consists of 3 files:

Core Structure

type token struct{}

type Group struct {
    cancel func(error)
    wg sync.WaitGroup
    sem chan token
    errOnce sync.Once
    err     error
}
Enter fullscreen mode Exit fullscreen mode
  • 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 used sync.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)
}
Enter fullscreen mode Exit fullscreen mode
  • 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)
                }
            })
        }
    }()
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode
  • 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
}
Enter fullscreen mode Exit fullscreen mode

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

Image description

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.

Image description

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Top comments (0)