Oluwatomisin Bamimore I'm a full-stack Python developer, technical writer, and a Section Engineering Education contributor.

Concurrency patterns in Golang: WaitGroups and Goroutines

4 min read 1317

Concurrency Patterns In Go: WaitGroups And Goroutines

Concurrency is a program’s ability to run more than one task independently in overlapping periods. In a concurrent program, several tasks can run at the same time in no particular order, which communicate, share resources, and interfere with each other.

With the rise of multicore CPUs and the ability to execute threads in parallel, developers can now build truly concurrent programs.

Golang provides goroutines to support concurrency in Go. A goroutine is a function that executes simultaneously with other goroutines in a program and are lightweight threads managed by Go.

A goroutine takes about 2kB of stack space to initialize. In contrast, a standard thread can take up to 1MB, meaning creating a thousand goroutines takes significantly fewer resources than a thousand threads.

In this tutorial, we will explore goroutines, communication between goroutines using channels, and syncing goroutines using WaitGroups.

Goroutines tutorial prerequisites

To follow and understand this tutorial, you need the following:

You can also clone this guide’s repository to access the complete template files or run the following in your terminal:

git clone https://github.com/Bamimore-Tomi/goroutines-logrocket.git

Creating goroutines in Golang

Adding the keyword go in front of a function call executes the Go runtime as a goroutine.

To demonstrate, let’s write a function that prints out random numbers, then sleeps. The first example is a sequential program and the second example uses goroutines:

go
package main
 
import (
    "fmt"
    "math/rand"
    "time"
)
 
// name is a string to identify the function call
// limit the number of numbers the function will print
// sleep is the number of seconds before the function prints the next value
func randSleep(name string, limit int, sleep int) {
    for i := 1; i <= limit; i++ {
        fmt.Println(name, rand.Intn(i))
        time.Sleep(time.Duration(sleep * int(time.Second)))
 
    }
 
}
func main() {
    randSleep("first:", 4, 3)
    randSleep("second:", 4, 3)
 
}
 
// OUTPUT
// first: 0
// first: 1
// first: 2
// first: 3
// second: 0
// second: 0
// second: 1
// second: 0
 
// git checkout 00

In this sequential run, Go prints the numbers in the order the function calls. In the following program, the functions run concurrently:

We made a custom demo for .
No really. Click here to check it out.

go
package main
 
import (
    "fmt"
    "math/rand"
    "time"
)
 
// name is a string to identify the function call
// limit the number of numbers the function will print
// sleep is the number of seconds before the function prints the next value
func randSleep(name string, limit int, sleep int) {
    for i := 1; i < limit; i++ {
        fmt.Println(name, rand.Intn(i))
        time.Sleep(time.Duration(sleep * int(time.Second)))
 
    }
 
}
func main() {
    go randSleep("first:", 4, 3)
    go randSleep("second:", 4, 3)
 
}
 
// git checkout 01

This program will not print anything out in the terminal because the main function completes before the goroutines execute, which is an issue; you don’t want your main to complete and terminate before the goroutines complete their execution.

If there is another sequential code after the goroutine, it runs concurrently until the sequential code completes its execution. The program then terminates regardless of completion.

go
package main
 
import (
    "fmt"
    "math/rand"
    "time"
)
 
// name is a string to identify the function call
// limit the amount of number the function will print
// sleep is the number of seconds before the function prints the next value
func randSleep(name string, limit int, sleep int) {
    for i := 1; i <= limit; i++ {
        fmt.Println(name, rand.Intn(i))
        time.Sleep(time.Duration(sleep * int(time.Second)))
 
    }
 
}
func main() {
    go randSleep("first:", 10, 2)
    randSleep("second:", 3, 2)
 
}
 
// second: 0
// first: 0
// second: 1
// first: 1
// first: 1
// second: 0
 
// git checkout 02

The program terminates after the function below the goroutine completes its execution, regardless of whether the goroutine completes or not.

To solve this issue, Golang provides WaitGroups.

WaitGroups in Golang

WaitGroup, provided in the sync package, allows a program to wait for specified goroutines. These are sync mechanisms in Golang that block the execution of a program until goroutines in the WaitGroup completely execute, as shown below:

go
package main
 
import (
    "fmt"
    "math/rand"
    "sync"
    "time"
)
 
// wg is the pointer to a waitgroup
// name is a string to identify the function call
// limit the number of numbers the function will print
// sleep is the number of seconds before the function prints the next value
func randSleep(wg *sync.WaitGroup, name string, limit int, sleep int) {
    defer wg.Done()
    for i := 1; i <= limit; i++ {
        fmt.Println(name, rand.Intn(i))
        time.Sleep(time.Duration(sleep * int(time.Second)))
 
    }
 
}
func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)
    go randSleep(wg, "first:", 10, 2)
    go randSleep(wg, "second:", 3, 2)
    wg.Wait()
 
}
 
// OUTPUT
 
// second: 0
// first: 0
// first: 1
// second: 1
// second: 1
// first: 0
// first: 1
// first: 0
// first: 4
// first: 1
// first: 6
// first: 7
// first: 2
 
// git checkout 03

Here, wg := new(sync.WaitGroup) creates a new WaitGroup while wg.Add(2) informs WaitGroup that it must wait for two goroutines.

This is followed by defer wg.Done() alerting the WaitGroup when a goroutine completes.

wg.Wait() then blocks the execution until the goroutines’ execution completes.

The whole process is like adding to a counter in wg.Add(), subtracting from the counter in wg.Done(), and waiting for the counter to hit 0 in wg.Wait().

Communicating between Goroutines

In programming, concurrent tasks can communicate with each other and share resources. Go provides a way for bidirectional communication between two goroutines through channels.

Bidirectional communication means either party can send or receive a message, so Go provides channels as the mechanism to send or receive data between goroutines.

You can create a channel by declaring or using the make function:

go
package main
 
import (
    "fmt"
)
 
func main() {
    // creating a channel by declaring it
    var mychannel1 chan int
    fmt.Println(mychannel1)
 
    // creating a channel using make()
 
    mychannel2 := make(chan int)
    fmt.Println(mychannel2)
 
}
 
// git checkout 04

Bidirectional channels in Go are blocking, meaning that when sending data into a channel, Go waits until the data is read from the channel before execution continues:

go
package main
 
import (
    "fmt"
    "sync"
)
 
func writeChannel(wg *sync.WaitGroup, limitchannel chan int, stop int) {
    defer wg.Done()
    for i := 1; i <= stop; i++ {
        limitchannel <- i
    }
 
}
 
func readChannel(wg *sync.WaitGroup, limitchannel chan int, stop int) {
    defer wg.Done()
    for i := 1; i <= stop; i++ {
        fmt.Println(<-limitchannel)
    }
}
 
func main() {
    wg := new(sync.WaitGroup)
    wg.Add(2)
    limitchannel := make(chan int)
    defer close(limitchannel)
    go writeChannel(wg, limitchannel, 3)
    go readChannel(wg, limitchannel, 3)
    wg.Wait()
 
}
 
// OUTPUT
 
// 1
// 2
// 3
 
// git checkout 04

With limitchannel <- i, the value of i enters the channel. fmt.Println(<-limitchannel) then receives the channel’s value and prints it out.

However, note that the number of sending operations must be equal to the number of receiving operations because if you send data to a channel and don’t receive it elsewhere, you get a fatal error: all goroutines are asleep - deadlock!.

Buffered channels

If you were wondering why you must always receive from a channel after sending, this is because Go does not have anywhere to store the values passed into the channel.

However, you can create a channel that stores several values, meaning sending data into that channel won’t block until you exceed the capacity:

go
limitchannel := make(chan int, 6)

This program sends data into a buffered channel and does not read it until the goroutine executes:

go
package main
 
import (
    "fmt"
    "sync"
)
 
func writeChannel(wg *sync.WaitGroup, limitchannel chan int, stop int) {
    defer wg.Done()
    for i := 1; i <= stop; i++ {
        limitchannel <- i
    }
 
}
 
func main() {
    wg := new(sync.WaitGroup)
    wg.Add(1)
    limitchannel := make(chan int, 2)
    defer close(limitchannel)
    go writeChannel(wg, limitchannel, 2)
    wg.Wait()
    fmt.Println(<-limitchannel)
    fmt.Println(<-limitchannel)
 
}
 
// OUTPUT
 
// 1
// 2
 
// git checkout 05

Conclusion

WaitGoups are just enough if you don’t need any data returned from a goroutine. However, you’ll often need to pass data around when building concurrent applications, which channels are extremely helpful for.

Understanding when to use channels is vital to avoid a deadlock situation and bugs, which can be extremely hard to trace. Sometimes, pointers and WaitGroups can achieve the purpose of a channel, but this is outside the scope of this article.

: Full visibility into your web and mobile apps

LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen, or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.

In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.

.
Oluwatomisin Bamimore I'm a full-stack Python developer, technical writer, and a Section Engineering Education contributor.

Leave a Reply