sync.WaitGroup for Goroutines

main() goroutine

The main() function is the main goroutine. It is one the main entry point in your program. The other one is init(). Which gets executed before main().

When the main() goroutine terminates, the program ends. If there’re goroutines in memory and main() terminates, so are the goroutines.

To get around this, we have to use something called a WaitGroup. It’s from the sync package.

Note: Types defined in the sync package should not be copied.

What is sync.WaitGroup

The purpose of a WaitGroup is to tell main() to wait for all goroutines to finish. It has 3 fields or properties:

  • Add(): Increment the WaitGroup counter by a given number(the number of goroutines), or call it each time you’re about to spawn a goroutine.

  • Done(): Should be called after each goroutine is finish to decrement the WaitGroup counter by one.

  • Wait(): Will blocks or waits(makes the function calling it to wait) until the counter becomes zero. Hence all goroutines are finished.

Goroutines

To start a goroutine, just call the function with the go keyword, and it will start concurrently with main():

go fmt.println("I am a goroutine")

Let’s try it:

package main

import "fmt"

func main(){
	go test()
	fmt.Println("I am MAIN")
	go test()
}


func test(){
	fmt.Println("I am a goroutine")
}

Go Playground

Only “I am MAIN” gets printed. main() exited before the goroutine test() is terminated. Now with a WaitGroup:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup // initialize WaitGroup

func main(){
	
	wg.Add(2) // increment the counter with the number of goroutines you want to spawn
	go test() // first goroutine
	fmt.Println("I am MAIN")
	go test() // second goroutine
	wg.Wait() // wait here till counter becomes zero, then continue
	fmt.Println("Now main() will exit.")
}


func test(){
	defer wg.Done() // decrement counter by one
	fmt.Println("I am a goroutine")
}

Go Playground

If the Add() counter is less than the number of spawned goroutines, the program will run fine. Because the ones that are finished(including main()), will just exit.

But the if the counter is greater than the number of spawned goroutines, you’ll get an error: fatal error: all goroutines are asleep - deadlock!. Instead of waiting forever, the go runtime will freeze the program and exits with a deadlock. Give it a try.

Tip: Learn more about deadlocks

Here’s another example, if you don’t know the specific number of goroutines:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var numOfG = 5 // maybe 5 is received from a command line flag or a function

func main() {

	wg.Add(numOfG)
	for i := 0; i < numOfG; i++{
		go test(3)
	}
	wg.Wait()
	fmt.Println("Done")
}

func test(num int) {
	defer wg.Done()
	for i := 0; i < num; i++ {
		fmt.Printf("Message %v\n", i)
	}
	
}

Go Playground

A slight change:

package main

import (
	"fmt"
	"sync"
)

var wg sync.WaitGroup
var numOfG = 5

func main() {
	// wg.Add(numOfG)
	for i := 0; i < numOfG; i++{
		wg.Add(1) // THE CHANGE
		go test(3)
	}
	wg.Wait()
	fmt.Println("Done")
}

func test(num int) {
	defer wg.Done()
	for i := 0; i < num; i++ {
		fmt.Printf("Message %v\n", i)
	}
	
}

“Don't be pushed around by the fears in your mind. Be led by the dreams in your heart.”Roy T. Bennett, The Light in the Heart