If you are just starting your journey about learning Go and how to implement highly concurrent, high-performance applications, then an understanding of WaitGroups is vital.
In this tutorial, we are going to be covering the following:
WaitGroupsare and when we should use them
- A simple example of working with
- A real world example of
By the end of this, you should have a solid grasp as to how to employ
WaitGroups within your own concurrent Go applications.
Let’s dive straight in and look at what a
WaitGroup is and what problem it solves for us.
When you start writing applications in Go that leverage
goroutines, you start hitting scenarios where you need to block the execution of certain parts of your code base, until these
goroutines have successfully executed.
Take for example this code:
It first prints out
Hello World before triggering a
goroutine and then finally printing out
When you run this however, you should notice that when you run this program, it fails to reach
line 6 and it never actually prints out
Inside my goroutine. This is because the main function actually terminates before the
goroutine gets a chance to execute.
The Solution? - WaitGroups
WaitGroups essentially allow us to tackle this problem by blocking until any goroutines within that
WaitGroup have successfully executed.
We first call
.Add(1) on our
WaitGroup to set the number of
goroutines we want to wait for, and subsequently, we call
.Done() within any
goroutine to signal the end of its’ execution.
Note - You need to ensure that you call
.Add(1)before you execute your
A Simple Example
Now that we’ve covered the essential theory, let’s take a look at how we can fix our previous example through the use of
As you can see, we’ve instantiated a new
sync.WaitGroup and then called the
.Add(1) method, before attempting to execute our
We’ve updated the function to take in a pointer to our existing
sync.WaitGroup and then called the
.Done() method once we have successfully finished our task.
line 19, we call
waitgroup.Wait() to block the execution of our
main() function until the
goroutines in the waitgroup have successfully completed.
When we run this program, we should now see the following output:
It should be noted that we can accomplish the same thing as above using anonymous functions should we so wish. This can be more succinct and easier to read if the goroutine itself isn’t too complex:
Again, if we run this it provides the same results:
But, things start to get a little more complex when we do things this way when we need to input arguments into our function.
Say for example, we wanted to pass a
URL into our function, we would have to pass that
URL in like so:
This is just something to keep in mind if you ever face this issue.
A “Real” World Example
In one of my production applications, I was tasked with creating an API that interfaced with a tonne of other APIs and aggregated the results up into one response.
Each of these API calls took roughly 2-3 seconds to return a response and due to the sheer number of API calls I had to make, doing this synchronously was out of the question.
In order to make this endpoint usable, I would have to employ
goroutines and perform these requests asynchronously.
However, when I first employed this tactic, I noticed that any calls I made to my API endpoint were returning before my
goroutines had a chance to complete and populate the results.
This is where I learned of
WaitGroups and dived deeper into the
By employing a
WaitGroup I could effectively fix this unexpected behavior, and only return the results once all of my
goroutines had finished.
Now that I’ve added the
WaitGroup to this endpoint, it will perform a
HTTP GET request on all of the URLs listed and, only upon completion, will return a response to the client calling that particular endpoint.
Note - There is more than one way to approach this problem, a similar effective outcome could have been achieve through the use of channels. For more on channels - Go Channels Tutorial
In this tutorial, we learned the basics of WaitGroups, including what they are and how we can use them within our own highly performant applications in Go.
If you enjoyed this tutorial or have any comments/suggestions, then please feel free to let me know in the comments/suggestions section below!
Note - If you wish to keep up to date with all the latest articles then I suggest following me on Twitter: @Elliot_F