VOOZH about

URL: https://blog.logrocket.com/concurrency-patterns-golang-waitgroups-goroutines/

⇱ Concurrency patterns in Golang: WaitGroups and Goroutines - LogRocket Blog


2021-12-03
1317
#go
Oluwatomisin Bamimore
80898
👁 Image

See how LogRocket's Galileo AI surfaces the most severe issues for you

No signup required

Check it out

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.

👁 Concurrency Patterns In Go: WaitGroups And Goroutines

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.

🚀 Sign up for The Replay newsletter

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.

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:

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.


Over 200k developers use LogRocket to create better digital experiences

👁 Image
Learn more →

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.


More great articles from LogRocket:


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.

Get set up with LogRocket's modern error tracking in minutes:

  1. Visit https://logrocket.com/signup/ to get an app ID
  2. 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>
     
  3. (Optional) Install plugins for deeper integrations with your stack:
    • Redux middleware
    • NgRx middleware
    • Vuex plugin
Get started now
👁 Image
👁 Image
👁 Image

Stop guessing about your digital experience with LogRocket

Get started for free

Recent posts:

How to build a virtual engineering team with Gemini CLI subagents

Learn how to use Gemini CLI subagents to delegate frontend, backend, testing, and docs tasks to specialized agents with guardrails and clear ownership.

👁 Image
Emmanuel John
Jun 18, 2026 ⋅ 10 min read

Debug Next.js apps with AI agents and next-browser

Learn how next-browser gives AI agents runtime context for debugging Next.js apps, including React props, hydration, PPR, forms, and performance.

👁 Image
Emmanuel John
Jun 17, 2026 ⋅ 9 min read

Stop hardcoding LLM SDKs: Dynamic LLM routing with OpenRouter and Next.js

Build dynamic LLM routing in Next.js with OpenRouter, TanStack AI, task classification, model fallbacks, and cost-aware routing.

👁 Image
Chizaram Ken
Jun 16, 2026 ⋅ 13 min read

What is TSRX?: What JSX would look like if it were designed today

TSRX adds first-class control flow, conditional hooks, and scoped styles to React via a TypeScript compiler extension — no new framework required.

👁 Image
Ikeh Akinyemi
Jun 12, 2026 ⋅ 6 min read
View all posts

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