My use case is to dispatch multiple long-running tasks to execute concurrently. The expectation is that the tasks will be IO-bound (e.g. network requests), but importantly each task is different. This is not a question about data parallelism.
I've written a simple pool type that wraps a sync.WaitGroup
and collects any errors that occur.
package gopool
import "sync"
// GoPool is a pool of goroutines with error reporting.
type GoPool struct {
waitGroup sync.WaitGroup
errors chan error
}
// New initializes a new GoPool.
func New() *GoPool {
return &GoPool{
waitGroup: sync.WaitGroup{},
errors: make(chan error),
}
}
// Run runs the specified function in a goroutine, collecting any errors that might occur.
func (p *GoPool) Run(goFuncs ...func() error) {
p.waitGroup.Add(len(goFuncs))
for _, goFunc := range goFuncs {
goFunc := goFunc
go func() {
defer p.waitGroup.Done()
err := goFunc()
if err != nil {
p.errors <- err
}
}()
}
}
// Wait waits for all specified tasks in the GoPoo to complete, and returns any collected errors that occurred.
func (p *GoPool) Wait() (errors []error) {
go func() {
p.waitGroup.Wait()
close(p.errors)
}()
for err := range p.errors {
errors = append(errors, err)
}
return errors
}
Here is an example of how it might be used:
func DoThings() {
var thingA int
var thingB string
var thingC *Thing
pool := gopool.New()
pool.Run(func() (err error) {
thingA, err = FetchThingA()
return err
})
pool.Run(func() (err error) {
thingB = FetchThingB()
return nil
})
pool.Run(func() (err error) {
thingC, err = FetchThingC()
return err
})
errs := pool.Wait()
if len(errs) > 0 {
// Handle errs
}
// Use each of the fetched things
}
This utility seems pretty useful, but I've not seen anyone write about it. Have I missed a major problem here, or is there a better way to handle this?