Demystifying Golang Channels, Goroutines, and Optimal Concurrency

The Framework

In my exploration of Golang, I wanted to take a deeper dive into some areas that I found interesting about the language. Though conceptually similar to the way other languages handle concurrency, I didn’t have much production-level knowledge surrounding goroutines and concurrency.

As an engineer, I have the compulsion to pull things apart and see how they work — and hopefully, gain a fundamental understanding of best practices and the contexts in which certain patterns make the most sense.

For goroutines and channels, I created an application using a Dispatcher -> Worker -> Job pattern to benchmark and compare results in differing scenarios. These comparisons would allow me to see how the same pattern operated under types of loads and hopefully uncover some situations where this was an optimal pattern to follow. Conversely, with the right array of scenarios, it’s just as important to know when this pattern does not provide the benefits I’m trying to achieve.

The code used in the following examples is available here.

The Question

After reading tutorials and documentation about channels and goroutines, I understood the theory and the concepts but didn’t know how I could predict when the usage of this type of pattern might be beneficial and when it might be harmful. Certainly, I knew that just jamming code into unlimited goroutines was not the answer. So, my question became:

When does concurrency in Golang make sense, and at what point are there diminishing returns?

Note: The examples below are based on my 8 Core development environment. Your runtime.NumCPU() value may be different.

An Idealized Scenario

While engineering always deals with the multivariate, eliminating as many variables as possible can bring you closer to the answer to a question. In trying to understand the basics of how Go executes goroutines and channels, I decided upon an idealized scenario in order to add as few variables to the equation as possible.

I decided to follow the Dispatcher -> Worker -> Job pattern using goroutines and channels. I started with this simple example and expanded on it.

A Rough Estimate

From my exploration of the topic, it seemed that the most efficient way to execute this pattern is to have a maximum of runtime.NumCPU() workers executing jobs. The runtime.NumCPU()function will give you the number of CPUs (or cores) available in your environment. This number should determine the optimal way to make use of your hardware.