In concurrent programming, handling synchronization is crucial to ensure smooth operation and correct results. In Go, sync.WaitGroup is a powerful synchronization tool to manage the execution of goroutines and wait for their completion before proceeding.

Understanding sync.WaitGroup

The sync.WaitGroup is part of Go's sync package. It provides a simple mechanism to wait for a collection of goroutines to finish without requiring complex logic or imposing performance overhead.

Key Methods of sync.WaitGroup

  • Add(int): Increment the wait group counter by the specified value.
  • Done(): Decrement the wait group counter by one. Typically called as a goroutine completes its task.
  • Wait(): Block execution until the counter becomes zero.

Using sync.WaitGroup: A Basic Example

Let's look at a basic example where we use sync.WaitGroup to manage three concurrent goroutines:

package main

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

func worker(id int, wg *sync.WaitGroup) {
  defer wg.Done() // Notify the WaitGroup that this worker is done
  fmt.Printf("Worker %d starting\n", id)
  time.Sleep(time.Second) // Simulate work with sleep
  fmt.Printf("Worker %d done\n", id)
}

func main() {
  var wg sync.WaitGroup

  for i := 1; i <= 3; i++ { // Start 3 workers
    wg.Add(1) // Increment the counter for each worker
    go worker(i, &wg)
  }

  wg.Wait() // Wait for all workers to complete
  fmt.Println("All workers completed.")
}

In the example above, we created three goroutines identified as worker 1, worker 2, and worker 3. The WaitGroup was used to track their completion and ensure that "All workers completed." message is printed only after all goroutines have finished.

Advanced Example: Error Handling and Synchronization

Sometimes, after launching goroutines, you may also want to handle errors returned by these goroutines. Here's a pattern you can use:

package main

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

func workerWithErr(id int, wg *sync.WaitGroup, errs chan<- error) {
  defer wg.Done()
  fmt.Printf("Worker %d starting\n", id)

  // Simulate some work with a potential error outcome
  if id == 2 { // Example condition where a worker might fail
    errs <- errors.New(fmt.Sprintf("Worker %d encountered an error", id))
  }

  fmt.Printf("Worker %d done\n", id)
}

func main() {
  var wg sync.WaitGroup
  errs := make(chan error, 3) // Capacity set to the number of goroutines

  for i := 1; i <= 3; i++ {
    wg.Add(1)
    go workerWithErr(i, &wg, errs)
  }

  wg.Wait() // Ensure all workers finish
  close(errs) // Close the error channel

  for err := range errs {
    if err != nil {
      fmt.Println(err)
    }
  }

  fmt.Println("All workers completed with error handling.")
}

In this example, the worker function might potentially return an error, which is pushed onto an error channel. The main function handles these potential errors after confirming all goroutines have finished.
This method does not halt execution mid-stream due to an error but awaits all tasks' completion, potentially valuable in scenarios immune to partial failures.

Conclusion

The sync.WaitGroup is among Go's most efficient tools for task synchronization. By utilizing sync.WaitGroup, you can systematically execute parallel computing processes and effectively incorporate error management patterns where applicable.