Harish V

Share this post

Concurrent API Calls and Race Conditions In Go

harishv.substack.com

Concurrent API Calls and Race Conditions In Go

A quick example of concurrent programming using Goroutines

Harish V
Aug 5, 2021
2
Share this post

Concurrent API Calls and Race Conditions In Go

harishv.substack.com

A quick example of concurrent programming using Goroutines

Starting line of race
Photo by Austris Augusts on Unsplash

Golang (or Go) was first released in 2009 and has been around for some time now.

It’s becoming wildly popular over time. At least that’s what I see from Stack Overflow trends, which is a good measure of how involved our developer community is on a technology/library/framework etc.

Chart: Since 2009, Go’s percent of Stack Overflow questions that month has risen to 0.50%

Go was developed by Google to help make concurrent programming easier, safer and more efficient. Lewis Fairweather has written an in-depth article on Golang here which summarises the motivation behind Go, the advantages and drawbacks.

In this article, we will examine one of the greatest features of Go: it’s support for concurrency.

We will try to execute multiple API calls and compare its execution time and also understand how simply we can implement concurrency in our programs.

The Base Code

In our program, we will make networks calls to fetch some data. For this purpose, I am going to use the cool Chuck Norris API to get some nice quotes in each call.

First, let’s create the directory for our project and use go mod init <module_path>/concurrent. Now, let’s create a main.go file. Let’s define the struct which stores the result from our API call. Here’s the code:

Let’s add a function to make the network call. It looks like the following:

Let’s add an auxiliary function which can print out our execution time since the start of program till completion.

func printExecutionTime(t time.Time) {
    fmt.Println("Execution time: ", time.Since(t))
}

This function takes in an argument t of type time.Time and calculates the time difference between current time and t.

Sequential Execution

We are going to fetch a total of 100 quotes. I am going to write a simple function that loops, fetches these quotes, and stores them in a map. The mapping is from the number of the call we are making to the received quote. The code looks like this:

Now, I am going to write my main function, which calls this.

func main() {
    startTime := time.Now()
    defer printExecutionTime(startTime)
    getQuotesSequentially(100)
}

Let’s run this by go run . in the directory via terminal. This will run all our get quotes sequentially and print out the execution time at the end.

Chuck Norris new qutoes

This took a total of 25.52 seconds on my computer! Wow, pretty slow, isn’t it?

Concurrent Execution

Now, let’s make our calls concurrent by using Goroutines and wait groups.

Goroutines are functions that are executed independently and simultaneously along with other Goroutines in the program. Though not exactly the same, it can be visualized as a lightweight thread.

Wait groups are used to help us keep track of the multiple Goroutines we are going to run. This prevents our program from exiting as soon as the main thread completes its execution.

Instead, having a wait group helps us to wait until all the Goroutines are complete fully.

Let’s create a function called GetQuotesConcurrently to implement this. This would look something like this:

Now, let’s run our main function call the above.

func main() {
    startTime := time.Now()
    defer printExecutionTime(startTime)
    getQuotesConcurrently(100)
}

And running this, we see the following:

More quotes

Wow, 1.58 seconds! That’s faster than sequential execution by 16 times!

Concurrent Execution (The Right Way)

However, we are not done yet.

Go actually provides a great way for us to check if we have a race condition in our code. Read this article to learn more.

If you run the same function above, but with a -race flag, go run -race .to check for race conditions, you would see this:

Note: For clarity, I have removed the fmt.Printf statement in the function.

We can see that we have made multiple attempts to write to the same map at the same time. This has led to a race condition where multiple goroutines are trying to access the same variable quotesMap at the same time.

This is not a safe operation and can potentially lead to further issues as our code evolves. To fix this, we will use a sync.Map, which allows us to synchronize write operations to the map we created.

As described in the documentation:

“Map is like a Go map[interface{}]interface{} but is safe for concurrent use by multiple goroutines without additional locking or coordination. Loads, stores, and deletes run in amortized constant time.”

Let’s update our function to use this.

Now, running our program again with go run -race ., we don’t see any errors.

Note: For clarity, I have commented out the fmt.Printf statement in the function.

The Full Code

Here’s the full code we have written so far with all the three main functions:

Conclusion

Yes, it’s that easy to utilize concurrency in Go programs! And the results speak for themselves, especially when we are dealing with a large number of asynchronous operations such as network calls.

Consider using Go’s amazing, simple, and efficient Goroutines to achieve your goals. At the same time, beware of race conditions. Use Go’s built-in race detection tools to check your code.

If you are ready to start creating your own APIs with Go, Ian Duncan has a great tutorial here where you can build a RESTful JSON API.

That’s all folks; happy coding!

Share this post

Concurrent API Calls and Race Conditions In Go

harishv.substack.com
Previous
Next
Comments
TopNew

No posts

Ready for more?

© 2023 Harish V
Privacy ∙ Terms ∙ Collection notice
Start WritingGet the app
Substack is the home for great writing