Concurrency bugs are some of the most frustrating issues in Go programming. You've probably been there: your program crashes with a panic, the stack trace points to sync.WaitGroup, and you realize you forgot a single Add() or Done() call. These mistakes are easy to make but painful to debug, especially in production.
Go 1.25 introduces WaitGroup.Go(), a method that eliminates an entire class of these errors by applying a principle from lean manufacturing: poka-yoke (mistake-proofing). Let's explore how this new functionality makes concurrent Go code safer and more maintainable.
The Traditional WaitGroup Pattern (and Its Pitfalls)
First, let's look at how we typically use WaitGroups:
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item string) {
defer wg.Done()
processItem(item)
}(item)
}
wg.Wait()
}
This pattern works, but it's fragile. Consider this common mistake:
Forgetting Done()
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Add(1)
go func(item string) {
// Oops! Forgot defer wg.Done()
processItem(item)
}(item)
}
wg.Wait() // This will block forever
}
The root cause? The API requires you to manually pair Add() and Done() calls, which violates the poka-yoke principle.
Enter WaitGroup.Go(): Poka-Yoke for Concurrency
Poka-yoke (ポカヨケ) is a Japanese term from lean manufacturing that means "mistake-proofing" or "error-proofing." It refers to any mechanism that prevents errors by design rather than relying on vigilance. Think of a microwave that won't start with the door open.
The new WaitGroup.Go() method applies this principle to concurrent programming:
func processItems(items []string) {
var wg sync.WaitGroup
for _, item := range items {
wg.Go(func() {
processItem(item)
})
}
wg.Wait()
}
Notice what's missing? No Add(), no Done(), no defer. The Go() method handles both automatically:
- It increments the counter (like
Add(1)) - It launches the goroutine
- It automatically calls
Done()when the function completes
You literally cannot forget. The API makes it impossible to create mismatched Add/Done pairs.
Benefits Beyond Safety
Beyond preventing bugs, WaitGroup.Go() offers additional advantages:
1. Cleaner Code
Less boilerplate means the business logic stands out:
// Traditional: 3 lines of ceremony
wg.Add(1)
go func() {
defer wg.Done()
doWork()
}()
// With Go(): 1 line
wg.Go(func() { doWork() })
2. Easier Code Reviews
Reviewers no longer need to verify Add/Done pairing. If the code compiles and uses Go(), it's correct.
3. Better for Beginners
New Go developers often struggle with WaitGroups. The Go() method reduces the learning curve significantly.
When to Use WaitGroup.Go()
Use WaitGroup.Go() when:
- Spawning goroutines in a loop
- Each goroutine represents an independent unit of work
- You want to wait for all goroutines to complete
- You don't need fine-grained control over when
Add()is called
Stick with traditional Add()/Done() when:
- You need to add multiple counts at once (
wg.Add(n)) - You're incrementing the counter conditionally
- You're working with legacy code that can't be easily refactored
Conclusion
WaitGroup.Go() is a perfect example of poka-yoke in API design. By making the right thing easy and the wrong thing impossible, Go 1.25 eliminates an entire category of concurrency bugs. The method doesn't add new functionality: you could always write correct WaitGroup code, but it makes correctness the default.
As Bjarne Stroustrup once said about C++: "Make simple things simple, and complex things possible." Go 1.25 takes this a step further for WaitGroups: it makes simple things safe, which is even better.
For more details, check out the official Go 1.25 release notes.
Ready to try it? Go 1.25 is available now. Start using WaitGroup.Go() in your next project and experience mistake-proof concurrency firsthand.
Written by

Kubilay Karpat
Contact