Synchronization with WaitGroups in Go

In Go, WaitGroups are a powerful tool for synchronizing concurrent operations, especially in scenarios where you need to wait for a group of goroutines to finish their tasks before proceeding. WaitGroups ensure that the main goroutine waits for all the spawned goroutines to complete their execution before terminating. Understanding WaitGroups is essential for writing robust and well-synchronized concurrent programs in Go.

Basics of WaitGroups

What is a WaitGroup?

A WaitGroup in Go is a synchronization primitive provided by the sync package. It allows goroutines to wait for a collection of other goroutines to complete their execution.

Creating a WaitGroup

To use a WaitGroup, you first create an instance of it using the sync.WaitGroup type and then add goroutines to the WaitGroup.

				
					var wg sync.WaitGroup

				
			

Adding Goroutines to the WaitGroup

Before starting a goroutine, you add it to the WaitGroup using the Add() method. Each call to Add() increments the WaitGroup’s counter.

				
					wg.Add(1)

				
			

Waiting for Goroutines to Finish

After starting a goroutine, you call the Done() method to decrement the WaitGroup’s counter when the goroutine completes its execution. The main goroutine can then wait for all goroutines to finish using the Wait() method.

				
					wg.Done()

				
			

Working with WaitGroups

Using WaitGroups

Let’s consider a simple example where we want to calculate the factorial of a number concurrently using multiple goroutines.

				
					package main

import (
	"fmt"
	"sync"
)

func factorial(n int, wg *sync.WaitGroup) {
	defer wg.Done()
	result := 1
	for i := 1; i <= n; i++ {
		result *= i
	}
	fmt.Printf("Factorial of %d: %d\n", n, result)
}

func main() {
	var wg sync.WaitGroup
	numbers := []int{3, 4, 5, 6}

	for _, num := range numbers {
		wg.Add(1)
		go factorial(num, &wg)
	}

	wg.Wait()
	fmt.Println("All calculations completed.")
}

				
			

In this example, we create a WaitGroup wg and iterate over a slice of numbers for which we want to calculate factorial concurrently. For each number, we add a goroutine to the WaitGroup using wg.Add(1), and inside the goroutine, we calculate the factorial and call wg.Done() when finished. Finally, we call wg.Wait() to wait for all goroutines to finish before printing “All calculations completed.

Advanced Concepts

WaitGroup with Error Handling

You can enhance WaitGroup usage by incorporating error handling. For instance, you can use channels to propagate errors from goroutines and handle them appropriately.

				
					package main

import (
	"errors"
	"fmt"
	"sync"
)

func doWork(wg *sync.WaitGroup, errChan chan<- error) {
	defer wg.Done()
	// Simulate some work
	if false { // Some condition that indicates an error
		errChan <- errors.New("an error occurred")
	}
}

func main() {
	var wg sync.WaitGroup
	errChan := make(chan error)

	for i := 0; i < 5; i++ {
		wg.Add(1)
		go doWork(&wg, errChan)
	}

	go func() {
		wg.Wait()
		close(errChan)
	}()

	for err := range errChan {
		fmt.Println("Error:", err)
	}
}

				
			

In this example, we create an error channel errChan to receive errors from goroutines. We add a goroutine to close the errChan after all other goroutines finish executing. Then, we use another goroutine to range over errChan and print any errors received.

WaitGroup with Timeout

You can use a combination of channels and select statements to implement a WaitGroup with a timeout feature. This allows you to wait for goroutines to finish within a specified time limit.

				
					package main

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

func worker(id int, wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Printf("Worker %d started\n", id)
	time.Sleep(time.Duration(id) * time.Second)
	fmt.Printf("Worker %d finished\n", id)
}

func main() {
	var wg sync.WaitGroup

	// Number of workers
	numWorkers := 5

	// Add goroutines to the WaitGroup
	for i := 1; i <= numWorkers; i++ {
		wg.Add(1)
		go worker(i, &wg)
	}

	// Wait for all goroutines to finish or until timeout
	timeout := 3 * time.Second
	done := make(chan struct{})
	go func() {
		wg.Wait()
		close(done)
	}()

	select {
	case <-done:
		fmt.Println("All workers completed.")
	case <-time.After(timeout):
		fmt.Println("Timeout occurred. Not all workers completed within the specified time.")
	}
}

				
			

In this example, we create a done channel to signal when all goroutines have finished executing. We start a goroutine to wait for all goroutines to finish and close the done channel. We then use a select statement to wait for either done to be closed or a timeout to occur.

Dynamic WaitGroup

Sometimes, you may not know the number of goroutines to wait for upfront. In such cases, you can use a dynamically managed WaitGroup by incrementing and decrementing the counter as needed.

				
					package main

import (
	"fmt"
	"sync"
)

func main() {
	var wg sync.WaitGroup

	// Dynamic number of goroutines
	numGoroutines := 5

	for i := 0; i < numGoroutines; i++ {
		wg.Add(1)
		go func(id int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d started\n", id)
			// Simulate some work
			fmt.Printf("Goroutine %d finished\n", id)
		}(i)
	}

	// Wait for all goroutines to finish
	wg.Wait()
	fmt.Println("All goroutines completed.")
}

				
			

In this example, we dynamically add goroutines to the WaitGroup in a loop. Each goroutine increments the WaitGroup counter upon execution and decrements it upon completion. The main goroutine waits for all dynamically added goroutines to finish before proceeding.

Exploring advanced concepts such as WaitGroup with timeouts and dynamically managed WaitGroups enhances your ability to handle complex synchronization scenarios in Go. By mastering these techniques, you can write highly efficient and robust concurrent programs that meet various requirements and constraints. Experimenting with different patterns and combinations further solidifies your understanding and expertise in concurrent programming with Go. Happy coding !❤️

Table of Contents

Contact here

Copyright © 2025 Diginode

Made with ❤️ in India