Concurrency in Golang

Mert Şakar
7 min readMar 17, 2022

In the real world things do not happen one step a time. Things change, interact, happen independently and simultaneously. So, in programming sequential processing is not enough to model world’s behavior. And right there, concurrency steps up to the stage and helps us to write our program to deal with the real world problems. In this article we will take a look at concurrency in golang.

Gopher image
Photo by Chinmay Bhattar on Unsplash

What Is Concurrency?

Concurrency is the composition of independently executing computations. It is a way to structure software, particularly as a way to write clean code that interacts well with the real world.

It’s not parallelism!!! Although it enables parallelism.

If you have one processor, your program can still be concurrent but it cannot be parallel.

Goroutines

A goroutine is simply a lightweight virtual thread of execution.

  • Independently executed function, launched by a go statement.
  • It has its own call stack, which grows and shrinks as required.
  • It’s very cheap. It’s practical to have thousands, even hundreds of thousands of goroutines.
  • It is not a thread.
  • There might be only one thread in a program with thousands of goroutines.

Let’s see with examples:

WaitGroups

Assume we have a code block like below

When we run main.go there will be only first counter printed on the console.

And then if we write go before first counter function that means run that function in background and continiou executing next line. Example below:

And the output will be like below:

But if we add go before the second function, there will be no output. Because main function is also a goroutine and when it finishes, the program exits.

The output: main function finishes, and program exits.

But if we add sleep for 2 seconds to main function, go routines will run for 2 seconds.

The output:

To keep program running you can use fmt.Scanln(). That way, program will be executed until a user input.

The output: Program runs until user presses enter.

In real life that example will not be useful because it requires manual control. Instead, we can use WaitGroup. To use WaitGroup we need to import sync package.

WaitGroup is basically a counter. You can add to the counter with wg.Add(). And you can decrease the counter with wg.Done(). Code block waits until the wg.Wait() line. When the wg counter is zero, program continious executing remaining lines.

In the example below we created an anonymous function to invoke it immediately. Since we used in inline, we could use wg.Done() in the function. We could also use WaitGroup in counter function, but we need to add an argument as a pointer of WaitGroup. So, I used it as below for our example.

The output:

Channels

Channels helps goroutines to communicate with each other.

There is go proverb: Don’t communicate by sharing memory; share memory by communicating. (You can also see more proverbs here.)

Pirate gopher passes information through channel.

In our example, our counter function outputs directly to the terminal. If we want to communicate back to main goroutine, we should use channels.

Channels are like speaking tubes in warships which you can send a message or receive a message (You can check it here). Channels need to have a type like string or int. Any type works with channels. You can even send channels through channels.

With channels execution on a goroutine waits until an information passes through the channel. For example:

In the example above, goroutine-1 waits at line 7 to have an information from goroutine-2. And vice versa, goroutine-2 waits at line 3 until goroutine-1 is ready to receive information.

I can hear you say enough of this cibberish. OK, let’s show the code:

As you can see we add a channel as a parameter to our counter function. And, we created a channel as c in the main function. The key point in here is the location of the arrow. If the arrow is on the left side, that means we are receiving information through channel. If the arrow is on the right side, that means we are sending information to our channel.

The output:

In our example, if we wrap our code in a for loop we will have a problem. Let’s see the code first:

The output: Deadlock!

As you can see in the output after a while we received a deadlock. That happened because the counter function is finished because for loop in the function loops until 7. But the main function is still waiting to receive information on the channel and nothing is going to send information to channel. Therefore, program will never be terminated. Go is able to detect this problem in runtime.

To solve this problem we can close the channel. As a sender, we will close the channel after we are done in the counter function. It is not a good practice to close the channel as receiver. Because, you don’t know whether the sender is finished or not. If, you close the channel before sender is done, sender will try to send on the closed channel and it will cause an error. Let’s see the code:

As you can see above, we closed the channel in counter function. And in the main function we added the open variable to see if the channel is open. If the channel is not open we break the for loop with the if statement.

The output:

Instead of doing it with infinite for loop and with if block with break, we can do it more beautiful with doing it for-range loop. Example below:

Let’s see another example to see channel constraints more clear.

Example: Try to send and receive on channels on main goroutine.

The output:

We are going to get deadlock again! This is because the send will block until something is ready to receive. But, the code doesn’t progress to receive line because we are blocked at send. To make it work, we need to receive in a seperate goroutine. Alternative to creating a new goroutine we can make a buffered channel by giving it a capacity. You can fill up a bufferend channel without a corresponding receiver and it will not block until the channel is full. Let’s see the code:

The output: It works!

We can also put two things without getting a deadlock. Example below:

https://gist.github.com/horzu/f24a338f50a17887ab6f50ae30ef6c1c

The output: It works again!

But if we want to send third, that will give us a deadlock again.

The output: a deadlock!

Select Statement

Let’s say we have two goroutines and two channels. First goroutine sends information to first channel every 500 milliseconds, and second goroutine sends information to second channel every 2 seconds. And main goroutine receives from those channels and prints those information. Let’s see the example:

We expect to see first channels information to show up every 500 milliseconds, and second channels information to show up every 2 seconds. But let’s see the output.

The output: They are coming sequentially!

First channel waits for the second channel to run. To overcome this we can use select statement. Let’s see the example with the select statement:

The output:

As you can see the second channel does not block the first channel.

Patterns

Let’s see a common pattern “worker pools”. This is used when you have a queue of work to be done. Multiple concurrent workers pulls items from the queue. Let’s see an example with fibonacci number generator function:

The output:

As you can see program prints out fibonacci numbers. But if you can look more closely you can see it is getting slower. So, we can add more workers to make it faster. Be careful! It will definately use more CPU.

Let’s see the example:

The output: Speed!

As you can see we could much more fibonacci numbers in less time. More workers, more computation and definalety more processeors!

That is briefly concurrency in golang. We learned goroutines, channels, select statements and worker pool pattern.

Thanks for reading this article until to end. If you want to get in contact, please don’t hesitate.

References:

--

--