WaitGroup
s and GoroutinesConcurrency 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 WaitGroup
s.
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
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:
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 WaitGroup
s.
WaitGroup
s in GolangWaitGroup
, 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()
.
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!
.
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
WaitGoup
s 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.
Install LogRocket via npm or script tag. LogRocket.init()
must be called client-side, not
server-side
$ npm i --save logrocket // Code: import LogRocket from 'logrocket'; LogRocket.init('app/id');
// Add to your HTML: <script src="https://cdn.lr-ingest.com/LogRocket.min.js"></script> <script>window.LogRocket && window.LogRocket.init('app/id');</script>
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.
One Reply to "Concurrency patterns in Golang: <code>WaitGroup</code>s and Goroutines"
Very Helpful…..