How to use WaitGroup in Golang

The selling point of the Go programming language is the built-in support for concurrency (driven by goroutine), a feature in the language used to power fast and performant software applications. In this shot, we will go over common problems with goroutines and solutions using the WaitGroup tool.

How Goroutines work

Goroutine is a thread-like chunk of code executed by the processora mechanism used to structure code that runs concurrently in Go applications. The code in Fig1 below demonstrates the synchronous execution of code, a term used to describe code that runs sequentially from top to bottom.

Fig1:

func main() {
  // main loop
  count("sheep")
  count("cow")
}

func count(thing string){
  for i := 0; true; i++ {
    fmt.Println(i, thing)
  }
}

The expression for i:= 0; true; i++ in Fig1 results in an infinite loop that prints count("sheep") to the terminal without having time to print count("cow"). This means that a true condition in the sequence never reached a false value to give room for the next function call, hence the continuous print of sheep to the terminal.

Fig2:

func main() {
  go count("sheep")
  go count("cow")
}

A function becomes a goroutine when the go keyword is put in front of it e.g.; go count("sheep").

While this function is executed in Fig2 above, it does not wait to finish before moving over to the next line of code in the infinite loop. Instead, it runs in the background, which creates a goroutine that runs concurrently. Altogether, we have two executing goroutines :

  • the main()function – serves as the main execution part of the program
  • go count("sheep").

Goroutines are very efficient but bear in mind that you can’t make a program infinitely fast by adding more goroutines because you are constrained by how many CPUs your system has.

Problems with Goroutines

When the code in Fig2 is edited slightly by adding go in front of the count("sheep") and count("cow"), it exposes the problem caused by goroutines.

At this point, the code execution returns no output, which may not be the behavior we want.

The problem is caused by the main goroutine terminating before the other two functions even begin execution. So, how can we solve this problem?

Code

package main
import (
"fmt"
"sync"
)
func main() {
//Goroutine execution 3
var wg sync.WaitGroup
wg.Add(2)
go func(){
count("sheep")
wg.Done()
}()
go func(){
count("cow")
wg.Done()
}()
wg.Wait()
}
func count(thing string){
for i:=0; i<=3; i++{
fmt.Println(thing)
}
}

Solution

The image above represents 2 states of our application while using WaitGroup.

Notice the go keyword in line 13 and 18, we can solve this output problem in 2 ways:

  1. By adding thetime.Sleep(time.Second) method at the end of the goroutine functions. The program will pause execution for a set duration while blocking the main() function, allowing other goroutines to execute. This solution is not scalable in a live project serving thousands of requests.

  2. You can use WaitGroup to solve the problem of empty output caused by goroutines in this case. To use this package, import sync.WaitGroup from the standard Golang primitive.

This package acts like a counter that blocks execution in a structured way until its internal counter becomes 0.

WaitGroup ships with 3 methods:

  1. wg.Add(int) method: The code in line 12 (above) indicates the number of available goroutines to wait for. The integer in the function parameter acts like a counter.

  2. wg.Wait() method: This method blocks the execution of code in line 22 until the internal counter reduces to a 0 value.

  3. wg.Done() method: This method decreases the count parameterused to indicate the termination of a goroutine in Add(int) by 1.

We declare 2 goroutine functions with the main function making it 3 goroutines altogether. Without the application of WaitGroup, the code will have no output.

The code in line 12 is the method used to count the number of goroutines to handle. The wg.Done() method in lines 15 and 20 decrements the counter by 1 at every function call, while wg.Wait() blocks to allow other goroutines to execute. This process prints the desired output. Take note of where these methods are positioned in the program.

Note that Channels are means by which different goroutines communicate. WaitGroups and Channels both solve a unique problem in organizing goroutines, but each use case depends on the project’s need.