Join me today, August 22, at 3:00pm EDT (19:00 UTC), more live coding. Subscribe on YouTube and hit the Notify me button so you don’t miss it!
We’ve been looking at different ways of managing multiple goroutines. But what if one (or more) the functions called in a goroutine might return an error? So far, we don’t have a solution to that in our toolbox.
There are a few different ways we can accomplish this. We’ll discuss a few, starting, today, with using a simple error value and a mutex.
First, a naïve implementation:
func doManyThings() error {
var wg sync.WaitGroup{}
var err error
for i := range 100 {
wg.Add(1)
go func() {
defer wg.Done()
err = doSomething(i)
}()
}
wg.Wait()
return err
}
This implementation contains two bugs, which we’ll discuss. But otherwise, it does a pretty good job of conveying the intention:
- We declare a variable, in the parent goroutine, to contain our error
var err error
- The anonymous function in each goroutine sets that
err
variable with the error returned bydoSomething(i)
- When all goroutines have completed, we return that error value.
You’ve likely picked up on the obvious error: err = doSomething(i)
unconditoinally overrites the error. This means that whichever goroutine completes last, will set the error, potentially ignoring any other errors that happened first. Let’s fix that bug first:
func doManyThings() error {
var wg sync.WaitGroup{}
var err error
for i := range 100 {
go func() {
wg.Add(1)
defer wg.Done()
if err2 := doSomething(i); err2 != nil && err == nil {
err = err2
}
}()
}
wg.Wait()
return err
}
The key change is:
if err2 := doSomething(i); err2 != nil && err == nil {
err = err2
}
Now we only set the final err
value if we receive an actual error from doSomething(i)
, and when err
is nil. In effect, we now report the first error to be encountered. This is fine in many cases. If you want to report all errors that occur, we’ll discuss approaches for that in the future.
The final bug that remains in the existing implementation is more subtle. It’s a datarace. The err
variable is not protected, which means it’s possible that two or more goroutines may attempt to read and/or write to it at the same time. While multiple simultaneous reads are safe, multiple writes, or simultaneous reads and writes are not safe!
Enter sync.Mutex
.
Consider this updated version:
func doManyThings() error {
var wg sync.WaitGroup{}
var mu sync.Mutex
var err error
for i := range 100 {
go func() {
wg.Add(1)
defer wg.Done()
if err2 := doSomething(i); err2 != nil {
mu.Lock()
if err == nil {
err = err2
}
mu.Unlock()
}
}()
}
wg.Wait()
return err
}
Now, before reading the err
value, we establish a lock. Only one active lock is permitted at a time on a sync.Mutex
value. If an active lock already exist, any subsequent calls to Lock()
will block until they are released. This ensures that at most a single goroutine will read or write the err
value at any given time.
After the function is done reading and/or writing the err
value, mu.Unlock()
releases the lock, so other goroutines may access that variable if needed.