New addition to the Go sync package since Go v1.25

March 7, 2026

Something I missed previously when reading the Go 1.25 release note in the “minor changes to the library” section: an entry for the sync package:

sync

The new WaitGroup.Go method makes the common pattern of creating and counting Goroutines more convenient.

It’s a small improvement that makes it harder to make mistakes so this is more than just convenience! Let’s see how it looks.

Example

Below I am assuming that you are already slightly familiar with Go concurrency and the sync package in the Go standard library. If not, you should start there first :)

The “classic” way: sync.WaitGroup.Add() + defer sync.WaitGroup.Done()

Before Go 1.25, the way to use sync.WaitGroup to count Goroutines was as follow:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Add(2)
	go func() {
		defer wg.Done()
		run(1, "A")
	}()
	go func() {
		defer wg.Done()
		run(5, "B")
	}()
	wg.Wait()
}

func run(iter int, name string) {
	for i := 0; i < iter; i++ {
		time.Sleep(time.Second)
		fmt.Println(name)
	}
}

The example is borrowed from Rob Reid’s blog post runtime.Goexit1 where he discusses a few alternatives way to implement this pattern, with the tools available in 2018.

One additional note: this code is assuming we don’t have access to the run function.

The “new” way: sync.WaitGroup.Go()

Rewriting the previous example for Go v1.25 and newer versions. This is also now the preferred way to use sync.WaitGroup according to the docs for Add and Done

Callers should prefer [WaitGroup.Go]

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package main

import (
	"fmt"
	"sync"
	"time"
)

func main() {
	var wg sync.WaitGroup
	wg.Go(func() {
		run(1, "A")
	}())
	wg.Go(func() {
		run(5, "B")
	}())
	wg.Wait()
}

func run(iter int, name string) {
	for i := 0; i < iter; i++ {
		time.Sleep(time.Second)
		fmt.Println(name)
	}
}

Comparison

The good things

  • we can get rid of the manual counting with sync.WaitGroup.Add(n) in the main Goroutine, this is automatically handled for you.
  • we can remove the call to defer sync.WaitGroup.Done() in each of the Goroutines, this is automatically handled for you.
  • the code looks the same wether we have access to the run function or not.

The negative I think is that by not using the keyword go directly, but instead using a normal library function that wraps it, when quickly looking over the code (like diagonal reading), you might miss the Goroutine creation since we are so used to see the go keyword highlighted in our favorite IDE. Would that be enough to not use it though? I don’t think so!

The end

A few more links to stay on the topic of Go concurrency


  1. runtime.Goexit by Rob Reid(2018) ↩︎

If you have questions, suggestions or want to discuss about this subject, I'd be more than happy if you reach me at conta-remove-ct@julienrouse.com. See also my open invite
Nifty tech tag lists from Wouter Beeftink