Avoiding Common sync.WaitGroup Mistakes in Go

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.

Changes to the go vet tool

The 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()

The Addition of the WaitGroup.Go Method

The 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:

  1. Adding a task to the WaitGroup
  2. Starting a goroutine that has a deferred call to wg.Done()
  3. Calling the original function passed into 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:

  1. We no longer need to use the go keyword to start a goroutine on our own. The wg.Go method handles that for us.
  2. We should NOT call 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.

In Summary…

To quickly summarize:

Learn Web Development with Go!

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.

    Avatar of Jon Calhoun
    Written by
    Jon Calhoun

    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.

    Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

    ©2024 Jonathan Calhoun. All rights reserved.