Better Programming

Advice for programmers.

Follow publication

Golang: How To Implement Concurrency With Goroutines and Channels

Axel Dietrich
Better Programming
Published in
6 min readFeb 6, 2022
As we can see, goroutines are concurrency on steroids.

A year ago I started working in a team developing and maintaining a core service for the shipping area of Mercado Libre, which is built with Go. My first experience (prior to getting this opportunity) with Go wasn’t really pleasant. Coming from Spring Boot, where everything is annotation-based, manually defining handlers seemed like a thing of yesterday. How wrong I was.

Go is a wonderful language. It is compiled to binary (in minimal compiling time, if I may add), incredibly performant (reasonably near to C, for a garbage collected language) and yet, really simple compared with most popular languages (25 keywords). Its syntax is similar to C’s with a few confusing elements but greatly appreciated once one becomes accustomed (multiple return values, naked returns, implicit data types, error checking, etc).

But Go’s best features in my humble opinion (and the topic of this article) are Goroutines and Channels.

Goroutines and Channels are a lightweight built-in feature for managing concurrency and communication between several functions executing at the same time. This way, one can write code that executes outside of the main program so it doesn’t interrupt it and returns a value or values (or nothing if it’s just an independent operation). Go has two keywords for this: go and chan.

The implementation is incredibly simple. First, you define a function that you want to execute concurrently.

As you can see, it’s just a regular function, nothing special. Now we execute it as a goroutine with the keyword go:

This gives us this console output:

We are executing a goroutine
Done!
9
Process finished with the exit code 0

Done, we now have a concurrently executing code. As you can see, the main program creates a new goroutine for executing timesThree function and continues with the next instruction. Therefore the fmt.Println("Done!") is executed before the goroutine.

But what happens if we need some value returning from that function to continue with our main flow? That’s where channels come in to save the day.

Channels

As their name tells us, are like two-ways streets for our data between goroutines. We have to initialize it with the function make, the keyword chan and the data type between parenthesis.

ch := make(chan dataType)

Let’s assume we need the result of the operation. Then we need to pass the channel as a parameter to the goroutine function so it returns the result with the characters <- for assigning the value.

Now this is what we get as console output:

We are executing a goroutine
9
The result is: 9
Process finished with the exit code 0

Once the main program executes the goroutines, it waits for the channel to get some data before continuing, therefore fmt.Println("The result is: %v", result) is executed after the goroutine returns the result. This doesn’t mean that the main program will wait for the full goroutine to execute, just until the data is served to the channel.

Now, what if we need the goroutine to return multiple values? That’s why we have buffered channels.

Buffered Channels

Let’s make our timesThree function receive an array of number and iterate over it multiplying every element by three

Let’s run it and see what we get:

We are executing a goroutine
The result is: 6
Process finished with the exit code 0

Why didn’t the main program print all 3 values? Well, because the channel only has space for one value. We can implement a buffered channel by assigning the channel capacity by passing a second parameter to the make function with the number of elements it can get before it’s read.

This way we can get all the values returned by the timesThree function.

We are executing a goroutine
Result: 6
Result: 9
Result: 12
Process finished with the exit code 0

Anonymous functions as goroutines

Another great feature is the possibility to execute an anonymous function as a goroutine, if we won’t reuse it. Note that we declare the function after the keyword go and we pass the parameters between parenthesis after the final curly brackets.

Channels between goroutines

Channels not only work for interactions between a goroutine and the main programs, they also provide a way to communicate between different goroutine. For example, let’s create a function that subtracts 3 to every result returned by timesThree but only if it’s an even number.

Console output:

We are executing a goroutine
The functions continues after returning the result
The functions continues after returning the result
Result: 3
Result: 9
Result: 9
Process finished with the exit code 0

Even if in this case it wasn’t necessary to have minusThree behave as a goroutine and have the result returned via the channel, it illustrates how the interaction between goroutines works. This is particularly useful when you have two different functions in a solution that needs to be performant and some condition of one affects the outcome of the other.

Range and close

These features enable us to receive continuous elements from a goroutine until it closes the channel. With the instruction, for i := range ch we can iterate over the goroutine’s results as soon as they are sent. The goroutine should close the channel with the function close once it finishes sending data.

If the goroutine doesn’t close the channel once it finishes sending data, the program will crash with the following error:

fatal error: all goroutines are asleep - deadlock!

This happens because the main program tries to receive a value from the channel but there isn’t an active goroutine able to send it.

I haven’t mentioned the close function before because as “The Go programming language” book states:

You needn’t close every channel when you’ve finished with it. It’s only necessary to close a channel when it is important to tell the receiving goroutines that all data have been sent. A channel that the garbage collector determines to be unreachable will have its resources reclaimed whether or not it is closed.

Select

How can we read from multiple channels at the same time? Works as a way to wait for multiple channels at the same time, preventing one from blocking another.

If we look at the console output we can see that the main program is receiving data from both goroutines at the same time.

We are executing a goroutine
Result minusThree: -1
Result timesThree: 6
Result minusThree: 0
Result timesThree: 9
Result timesThree: 12
Result timesThree: 15
Result minusThree: 1
Result timesThree: 18
Result minusThree: 2
Result minusThree: 3
Process finished with the exit code 0

We can add a default case (like in a switch statement) if we want to execute something else every iteration that there is no data from any channel.

Mutual exclusion

A problem that may arise when working with concurrency is when two gshare the same resources, which shouldn’t be accessed at the same time by multiple goroutines.

In concurrency, the block of code that modifies shared resources is called the critical section. Let’s illustrate with an example and what it prints on console.

We are executing a goroutine
27
81
3
9
243
6561
729
19683
59049
2187
Process finished with the exit code 0

Because goroutines are accessing and reassigning the same memory space at the same time, we get problematic results. In this case, n *= 3 would be the critical section.

We can easily solve this problem by locking the block of code that access the variable by implementing mutual exclusion using sync.Mutex . This prevents that more than one goroutine accesses the instructions between the Lock() and Unlock() functions at the same time.

Now we get the powers of three as output:

We are executing a goroutine
3
9
27
81
243
729
2187
6561
19683
59049
Process finished with the exit code 0

Conclusion

Goroutines are an amazing and lightweight feature that makes concurrency pretty easy to implement, one of the reasons Go’s adoption hasn’t stopped to rise in the recent years (and why lately I have been loving it so much).

Thanks a lot for reading!

Make sure to follow me so you don’t miss my upcoming stories!

Axel Dietrich
Axel Dietrich

Written by Axel Dietrich

I'm a professional software developer looking to learn and share through blogging

Responses (4)

Write a response