A look at concurrency primitives in Go

Let’s take a look at concurrency primitives in Go: Goroutines, Channels and WaitGroups. Here is a very simple program with no concurrency features which we will use to iterate on. We call two functions, each printing a statement and sleeping for a second, from the main function, which also prints a statement and the time it took the program to run in the end.

package main

import (
	"fmt"
	"time"
)

func main() {
	now := time.Now()
	defer func() {
		fmt.Println(time.Since(now))
	}()

	fmt.Println("Hi from main function!")
	one()
	two()
}

func one() {
	fmt.Println("Hi from function one!")
	time.Sleep(time.Second)
}

func two() {
	fmt.Println("Hi from function two!")
	time.Sleep(time.Second)
}

Output:

Hi from main function!
Hi from function one!
Hi from function two!
2.000129487s

As expected, the functions execute in order and take slightly above 2 seconds to execute, the time they sleep. But imagine we want function one and two to be run concurrently, in Go we just add ‘go’ in front of the function call:

	fmt.Println("Hi from main function!")
	go one()
	go two()

Output:

Hi from main function!
15.99µs

Oops! Only the stament from the main function gets printed and the time the program takes to run is only ~16 microseconds! This happens because when we call a function as a goroutine with ‘go’, the main function (also a goroutine!) doesn’t wait on the called function to actually execute. This is where WaitGroups come in play! We create a WaitGroup with the sync package, increment the counter by two before we call our two goroutines, pass the WaitGroup as a pointer to the functions we run as goroutines and decrement the counter by one inside each function. In the end, we tell our WaitGroup to wait until all goroutines have run (or rather the counter got decremented to 0).

func main() {
	now := time.Now()
	defer func() {
		fmt.Println(time.Since(now))
	}()

	var wg sync.WaitGroup

	fmt.Println("Hi from main function!")
	wg.Add(2)
	go one(&wg)
	go two(&wg)
	wg.Wait()
}

func one(wg *sync.WaitGroup) {
	fmt.Println("Hi from function one!")
	time.Sleep(time.Second)
	wg.Done()
}

func two(wg *sync.WaitGroup) {
	fmt.Println("Hi from function two!")
	time.Sleep(time.Second)
	wg.Done()
}

Output:

Hi from main function!
Hi from function two!
Hi from function one!
1.000056387s

All the statements get printed and it only took the program ~1 seconds to execute instead of ~2 because we ran functions one and two concurrently. But functions one and two got executed in different order! Depending on use case you might not care about the order, but let us assume we do care in this case and we want to synchronize the goroutines. We will use channels to achieve this. Let us also add a third function. Remember to increment the WaitGroup to 3. We add a channel ‘done’ that accepts boolean values, we pass it to our functions as a parameter and send ’true’ to the channel inside each function. In the main function we wait before calling functions two and three until we receive true from the channel:

func main() {
	now := time.Now()
	defer func() {
		fmt.Println(time.Since(now))
	}()

	var wg sync.WaitGroup

	done := make(chan bool)

	fmt.Println("Hi from main function!")

	wg.Add(3)
	go one(&wg, done)
	<-done
	go two(&wg, done)
	<-done
	go three(&wg, done)
	<-done
	wg.Wait()
}

func one(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function one!")
	time.Sleep(time.Second)
	done <- true
	wg.Done()
}

func two(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function two!")
	time.Sleep(time.Second)
	done <- true
	wg.Done()
}

func three(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function three!")
	time.Sleep(time.Second)
	done <- true
	wg.Done()
}

Output:

Hi from main function!
Hi from function one!
Hi from function two!
Hi from function three!
3.000301256s

We see that the functions now execute in order but they take ~3 seconds, it should take ~1! That happens because we send true to channel done after we called time.Sleep inside each function, if we call it before we call time.Sleep …

func one(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function one!")
	done <- true
	time.Sleep(time.Second)
	wg.Done()
}

func two(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function two!")
	done <- true
	time.Sleep(time.Second)
	wg.Done()
}

func three(wg *sync.WaitGroup, done chan bool) {
	fmt.Println("Hi from function three!")
	done <- true
	time.Sleep(time.Second)
	wg.Done()
}

Output:

Hi from main function!
Hi from function one!
Hi from function two!
Hi from function three!
1.000673424s

Yay, our functions execute in order and concurrently, mission accomplished. This was a first look at basic concurrency primitives in Go, I hope to learn more advanced patterns in the future and I’m sure I will blog about it when time comes.