With the latest release of Go 1.25, there are two notable changes to the sync.WaitGroup
type, both of which stem from common mistakes that developers make when using the WaitGroup
type. In this post we are going to explore both of these changes and how they can help you avoid a common race condition.
go vet
toolThe first change isn’t a change to the language, and is instead an addition to the go vet
tool. go vet
now has checks to prevent error-prone calls to the WaitGroup.Add function.
To really understand this change, we need to first to look at the type of problem this was intended to fix.
Imagine that we were writing a Go program that starts a few goroutines to do some work.
for i := 0; i < 5; i++ {
go func() {
fmt.Printf("Worker %d: Working...\n", i)
time.Sleep(200 * time.Millisecond) // pretend to do work
fmt.Printf("Worker %d: Finished.\n", i)
}()
}
We probably want to wait for all of the goroutines to finish their work, which is a great use case for the sync.WaitGroup type, so we change our code to the following.
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
go func() {
wg.Add(1)
defer wg.Done()
fmt.Printf("Worker %d: Working...\n", i)
time.Sleep(200 * time.Millisecond) // pretend to do work
fmt.Printf("Worker %d: Finished.\n", i)
}()
}
wg.Wait()
At first glance this might look okay, but there is a subtle bug in our code. We are calling wg.Add(1)
inside the goroutine, which can lead to unexpected results.
This error occurs because some or all of our goroutines might not start running before our code reaches the wg.Wait()
call. If that occurs, then our program will not wait for all of the goroutines to finish before proceeding.
In Go 1.24, the go vet
tool didn’t detect this type of issue, but in 1.25 it now will highlight the mistake.
# Make sure we're using Go 1.25
go vet
# github.com/joncalhoun/waitgroup125
# [github.com/joncalhoun/waitgroup125]
./main.go:13:10: WaitGroup.Add called from inside new goroutine
The fix for this particular case is to move the wg.Add
call outside of the goroutine. This ensures that all of the things we want to wait on are tracked by the WaitGroup
before we reach the wg.Wait()
call.
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
fmt.Printf("Worker %d: Working...\n", i)
time.Sleep(200 * time.Millisecond) // pretend to do work
fmt.Printf("Worker %d: Finished.\n", i)
}()
}
wg.Wait()
WaitGroup.Go
MethodThe second change is the Go method which was added to the sync.WaitGroup
type. This is a helper method that helps reduce bugs like the one we saw above by handling both the wg.Add
and wg.Done
calls.
Below is the same program from before, but it is now using the wg.Go
method.
for i := 0; i < 5; i++ {
wg.Go(func() {
fmt.Printf("Worker %d: Working...\n", i)
time.Sleep(200 * time.Millisecond) // pretend to do work
fmt.Printf("Worker %d: Finished.\n", i)
})
}
wg.Wait()
To understand how this works, we are going to look at the implementation of the WaitGroup.Go
method in the source code
func (wg *WaitGroup) Go(f func()) {
wg.Add(1)
go func() {
defer wg.Done()
f()
}()
}
In this code we can see that wg.Go
is:
wg.Done()
wg.Go
If we were to expand this out, we would find that it is equivalent to the code we were writing before with our WaitGroup, but it takes the boilerplate that we always end up writing and does it for us.
I suspect this will help a lot of people avoid the type of mistakes that go vet
was updated to catch, but there are two important things to note when writing code using the wg.Go
method:
go
keyword to start a goroutine on our own. The wg.Go
method handles that for us.wg.Done
in the function passed to wg.Go
. This will also be handled by the wg.Go
method for us.If we add the go
keyword before wg.Go
we introduce a bug similar to the one we discussed with the go vet
changes. That is, our wg.Add
call will now be running inside of a goroutine and depending on the timing might not get run before we call wg.Wait()
.
If we add a call to wg.Done()
to our code inside of the wg.Go
function call we will end up calling Done twice for every task, which will result in issues. In some cases this will result in a panic, but it is also possible for our program to terminate while some goroutines are still running. This is shown in the following example.
func wgGo_extraDone() {
var wg sync.WaitGroup
for i := 0; i < 6; i++ {
wg.Go(func() {
// Do NOT do this inside wg.Go!
defer wg.Done()
fmt.Printf("Worker %d: Working...\n", i)
variableSleep(i) // pretend to do work
fmt.Printf("Worker %d: Finished.\n", i)
})
}
wg.Wait()
}
func variableSleep(t int) {
time.Sleep(time.Duration(t*2000) * time.Millisecond)
}
Interestingly, go vet
doesn’t appear to detect either of these bugs, so hopefully the addition of wg.Go doesn’t lead to developers commonly making a new mistake.
To quickly summarize:
go vet
now tries to detect additional bugs when using sync.WaitGroup
.wg.Go
helps simplify using sync.WaitGroup
by handling most of the boilerplate for us.Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.
You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.
Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.
Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.
Related articles
Spread the word
Did you find this page helpful? Let others know about it!
Sharing helps me continue to create both free and premium Go resources.
Want to discuss the article?
See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!
You can reach me via email or via twitter.
©2024 Jonathan Calhoun. All rights reserved.