How and when to use sync.WaitGroup in Golang

Picture of hirok.biswas
hirok.biswas
Published on
29.08.2024
Time to Read
2 min

[rank_math_breadcrumb]

golang waitgroup
Table of Contents
Golang is known for its first-class support for concurrency, or the ability for a program to deal with multiple things at once. Running code concurrently is becoming a more critical part of programming as computers move from running a single code stream faster to running more streams simultaneously. Goroutines solve the problem of running concurrent code in a program, and channels solve the problem of communicating safely between concurrently running code. Read details concept of Goroutines and Channels Concurrency in GO 101 – goroutine and channels In that article, we looked at using channels in Go to get data from one concurrent operation to another. But we also ran across another concern: How do we know when all the concurrent operations are complete? One answer is that we can use sync.WaitGroup.

Waitgroup

Allows you to block a specific code block to allow a set of goroutines to complete their executions. WaitGroup ships with 3 methods:
  •  wg.Add(int): Increase the counter based on the parameter (generally “1”).
  •  wg.Wait(): Blocks the execution of code until the internal counter reduces to 0 value.
  •  wg.Done(): Decreases the count parameter in Add(int) by 1.
Example:
				
					package main

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

var taskIDs =[]int32{1, 2, 3}

func main() {
    var wg sync.WaitGroup
    for _, taskID := range taskIDs {
        wg.Add(1)
        go performTask(&wg, taskID)
    }
    // waiting until counter value reach to 0
    wg.Wait()
}

func performTask(wg *sync.WaitGroup, taskID int32){
    defer wg.Done()
    time.Sleep(3 * time.Second)
    fmt.Println(taskID, " - Done")
}
				
			
In the above example, we have simply initiated 3 goroutines (we can also initiate more goroutines as we want). Before initiating every goroutine wg.Add(1) increases the counter by 1. From inside goroutine defer wg.Done() executes after all functionality is done and decreases the counter value by 1. And finally wg.Wait() releases the main block when the counter value reached to 0. So, WaitGroup ensures the completion of all initiated goroutine’s execution.

When to use Waitgroup?

  • The major use case is to simply wait for a set of goroutines untill complete their execution.
  • Another is to use along with channel(s) to achieve better outcomes.
For a better explanation of 2nd use case let’s solve a real-world problem. Assume we have 7 order ids. using concurrency we have to collect successful order details of those orders.
At First, we will go for channel approach to collect individual order details.
				
					package main
import (
    "fmt"
    "time"
)

var orderIDs = []int32{1, 2, 3, 4, 5, 6, 7}
type order struct {
    id   int32
    time string
}

func main() {
    var orders []order
    ch := make(chan order)

    for _, orderID := range orderIDs {
        go getOrderDetails(ch, orderID)
    }

    // iterate over length of orderIDs and append orders
    for i := 0; i < len(orderIDs); i++ {
        orders = append(orders, <-ch)
    }

    fmt.Printf("collected orders: %v", orders)
}

func getOrderDetails(ch chan order, orderID int32) {
    time.Sleep(3 * time.Second)
    // details collection logic
    orderDetails := order{id: orderID, time: time.Now().UTC().Format("15:04:05")}

    ch <- orderDetails
}
				
			
Here, we have created an unbuffered channel ch. Every goroutine collects order details and sends data to the channel. Same time for collecting order details we have iterated over the length of expected order ids to receive order data one by one from the channel for every order ids. As far the program works fine as expected and the output is:
Output:
				
					collected orders: [{3 23:00:03} {1 23:00:03} {7 23:00:03} {4 23:00:03} {2 23:00:03} {6 23:00:03} {5 23:00:03}]
				
			
But, suppose one of our goroutine returns an error instead of sending order data to the channel. On the other hand, our channel is trying to receive data but data is not available in the channel. In this situation program will enter a deadlock situation. Try this scenario here Now we will combine channel and waitgroup to collect successful order data.
				
					package main

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

var orderIDs = []int32{1, 2, 3, 4, 5, 6, 7}
type order struct {
    id   int32
    time string
}

func main() {
    var wg sync.WaitGroup
    var orders []order

    // create a buffered channel wit length of orderIDs
    ch := make(chan order, len(orderIDs))

    for _, orderID := range orderIDs {
        wg.Add(1)
        go getOrderDetails(ch, &wg, orderID)
    }
    // here we wait until all jobs done
    wg.Wait()
    // closing channel after all job done
    close(ch)

    // iterate over available channel results
    for v := range ch {
        orders = append(orders, v)
    }

    fmt.Printf("collected orders: %v", orders)
}

func getOrderDetails(ch chan order, wg *sync.WaitGroup, orderID int32) {
    defer wg.Done()

    time.Sleep(3 * time.Second)
    // details collection logic
    orderDetails := order{id: orderID, time: time.Now().UTC().Format("15:04:05")}
    
    if orderID == 4 {
        fmt.Println("Something went wrong")
        return
    }

    ch <- orderDetails
}
				
			
Here we have created a buffered channel having max buffer size of orders ids length. From goroutine, order data will be sent to channel(not necessary all order data will be sent, in case of error). Here for order id 4 no data is sent to the channel but wg.Done() executed as expected. After wg.Wait() reached its ends, we have closed the channel as all goroutine’s job done. Then from the channel, we have simply collected available order data by iterating over available channel results. In our case, there were 7 incoming order ids and we finally got 6 successful orders detail from the channel without any error.
Output:
				
					collected orders: [{5 23:00:00} {6 23:00:00} {3 23:00:00} {1 23:00:00} {7 23:00:00} {2 23:00:00}]
				
			

Final thoughts:

Channels being a signature feature of the Go language shouldn’t mean that it is idiomatic to use them alone whenever possible. What is idiomatic in Go is to use the simplest and easiest way to understand the solution. The WaitGroup conveys both the meaning (the main function is Waiting for workers to be done) and the mechanic (the workers notify when they are Done).
50+ companies rely on our top 1% talent to scale their dev teams.
Excellence Our minimum bar.
It has become a prerequisite for companies to develop custom software.
We've stopped counting. Over 50 brands count on us.
Our company specializes in software outsourcing and provides robust, scalable, and efficient solutions to clients around the world.
klikit

Chris Withers

CEO & Founder, Klikit

Klikit-logo
Heartfelt appreciation to Vivasoft Limited for believing in my vision. Their talented developers can take any challenges against all odds and helped to bring Klikit into life.appreciation to Vivasoft Limited for believing in my vision. Their talented developers can take any challenges.
Start with a dedicated squad in 7 days

NDA first, transparent rates, agile delivery from day one.

Where We Build the Future
Scale Engineering Without the Overhead

Elastic offshore teams that integrate with your processes and timezone.

Tech Stack
0 +
Blogs You May Love

Don’t let understaffing hold you back. Maximize your team’s performance and reach your business goals with the best IT Staff Augmentation

let's build our future together

Get to Know Us Better

Explore our expertise, projects, and vision.