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.
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
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 WaitGroups.
WaitGroups 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
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.
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>

CSS text-wrap: balance vs. text-wrap: prettyCompare and contrast two CSS components, text-wrap: balance and text-wrap: pretty, and discuss their benefits for better UX.

Remix 3 ditches React for a Preact fork and a “Web-First” model. Here’s what it means for React developers — and why it’s controversial.

A quick guide to agentic AI. Compare Autogen and Crew AI to build autonomous, tool-using multi-agent systems.

Compare the top AI development tools and models of November 2025. View updated rankings, feature breakdowns, and find the best fit for you.
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 now
One Reply to "Concurrency patterns in Golang: <code>WaitGroup</code>s and Goroutines"
Very Helpful…..