Concurrency in Go — Using Channels and Handling Race Conditions

martin cartledge
Aug 5, 2020 · 7 min read

This is the twelfth entry of my weekly series Learning Go. Last week I talked about using Goroutines and WaitGroups to make use of concurrent patterns in Go. This week I will continue to expand on the topic of concurrency by showing you how to use a few more useful features of Go: Channels and Mutex. I will also walk you through how you can identify and fix potential race conditions in your code. Let’s get to it!

Channels

a “pipe” that allows you to send and receive values in your concurrent Goroutines

Essentially, Channels can be thought of as messengers that you can send to various Goroutines to deliver or retrieve values

  • Channels block

Channels have two important pieces of syntax <- and ->

When you want to send data onto a Channel you use this syntax channel <-

When you want to retrieve data from a Channel you use this syntax <- channel

Let me show you a quick example to see Channels in action

Inside of our func main we use the short declaration operator to create a new variable with the identifier c. We use the make keyword to create a new chan which will be of type int

Next, we create an anonymous Goroutine. Inside of this Goroutine, using the c <- syntax we are sending the value 29 onto our c Channel. We use the () to immediately run this Goroutine

We use the <- c syntax to get the value off of our channel and print the value of c. When we go, it should be no surprise that we get the value 29. Cool! We have successfully sent data to a Channel and received a value from a Channel.

What if we want to limit the amount of data that can be sent onto a Channel we create? Go makes this possible by using a feature called Channel Buffering

By default, Channels are unbuffered, meaning that there is no limit to the amount of data that can be sent and stored on a Channel. As you can imagine, this could get out of hand fairly quickly. Whenever possible, it is always a good idea to use Channel Buffering.

Let me show you an example:

This should right be a surprise, right? As you can see we have 5 messages sent to chat and have successfully used fmt.Println() to print out each entry. But what happens if we try to add another entry to chat?

But what happens if I try to add another string value to our Channel? Let’s take a look:

This is the same code, except for this time we are trying to add No spoilers please! onto our chat Channel. Go does not like this. When we try to run our application, Go gives us this error:

Although errors are commonly looked to as nuisances, Go thoughtfully introduces them to help you prevent unwanted side effects in your application.

Directional Channels

When you start to use Channels, you quickly realize how helpful creating Channels that can only receive values or only send values. Go bakes in this feature into the chan type, let me show you a few examples:

Inside of func main we create a new variable with the identifier c which is of type chan which will contain values of type int

Notice we make c a buffered channel by passing the value 1 as the second argument to make. In this case, our buffered channel can only contain one value.

Next, we send the value 29 onto our c Channel

This seems pretty straight forward, doesn’t it? When fmt.Println(<-c) is executed we should expect the value 29 to be printed, right? Nope. This is Directional Channels doing their job. Remember when we created our c Channel? We explicitly told Go that this c Channel would only be a sending Channel when we used this syntax chan <- int

When we attempt to run this code, Go stops us and gives us a very informative error message

Let me show you another example of using a Directional Channel

In this example, we create a new variable with the identifier c which is of type chan which will contain values of type int; however, in this case, we are creating a Directional Channel that can only receive values

Now, when we try to send the value 29 onto the c Channel, we get this error message

Directional Channels can play an important role in organizing your code. Now, you can explicitly create a Sending Channel and a Receiving Channel. If you add the peace of mind of making both of these Channels buffered, then you will gain a lot of predictability in your code.

Let me show you a more in-depth example of using buffered, directional channels

In this example we use a buffered channel and we create two functions: one that sends a value onto our channel and one that receives a value from our channel

First things first. We create a new variable with the identifier c which is of type chan which will contain values of type int. the 1 argument in our make function makes c a buffered channel

Next, we call a function with the identifier send and we pass our c channel as the only argument, we do the same for a function with the identifier receive

We create a function with the identifier send that has a single argument that is a sending channel (channel that you can only send values to). This channel will contain values of type int. Inside of this function we send the value 29 onto our c channel

We create a function with the identifier receive that has a single argument that is a receiving channel (a channel that you can only receive values from). This channel will contain values of type int. Inside of this function we receive the value 29 onto our c channel and print that value using the fmt package

Mutex and Race conditions

So, we have learned about sending and receiving data across Channels, but what happens if multiple Goroutines need to access a shared piece of state? The reliability of our state can be compromised very easily, we do not want that.

Let me show you an example of when multiple Goroutines using a shared piece of state can provide unwanted results

Note: For the sake of this example, I am going to be using the runtime package to make use of the Gosched() method. This method will allow me to fire off a new Goroutine.

I want to make sure that I am importing the packages I am going to make use of; therefore, I import fmt, runtime, and the sync package

Inside of func main, we declare a new variable with the identifier counter that is assigned to the value 0

Next, I create a variable with the identifier gs (this stands for Go Schedule, I will cover this shortly) that is assigned to the value 5

I create a variable with the identifier wg and assign it the value of a new WaitGroup

I am going to use the value of gs to iterate; therefore, I want to make sure that I Add 5 WaitGroups

Next, we create a for loop that will use the value of gs in the condition statement. Inside of this for loop we create an anonymous _Goroutine_ and immediately invoke the Goroutine.

Inside of our anonymous Goroutine we declare a new variable with the identifier v and assigned the value of counter

We use the runtime package to invoke the Gosched() method, thus firing a new Goroutine

On the next line, we increment our v variable and assign the counter variable to the v variable

We use the fmt package to print out the value of counter and use the wg variable to call the Done() method which will let the Go runtime know that our WaitGroup is complete

We repeat this process 5 times total

Outside of the for loop, we call the Wait() method that we access from the wg variable. The Wait() method prevents our main function from exiting

Once our WaitGroups are all complete our main function exits

Did you notice what our for loop prints for the value of counter? Currently, the value is 1 each iteration. Why is that?

Currently, we are immediately invoking 5 Goroutines and not telling the Go runtime what to do with them; therefore, they are running, accessing, and updating our counter state at random. There is no way to predict the order of these Goroutines. There is a way to fix this though!

The concept of only wanting a single Goroutine to access a piece of state at a time, thus avoiding conflicts is called Mutual Exclusion. The traditional name for the data structure that shares this methodology is called a Mutex

The sync package, provided by the Go standard library, offers us sync.Mutex with two important methods: Lock and Unlock

Let me show you an example of using Mutex

The code above is the same except for in a few places. Let me illuminate the differences in our code while using a Mutex

In order for us to use a Mutex, we create a new variable with the identifier mu that has a value of a Mutex which we get from the sync package

On the first line inside of our anonymous Goroutine we use our mu variable to call the Lock() method. This method ensures exclusive access to our state. Once we are done updating our state, we call the Unlock() method which is also supplied from our mu variable

Now we can see that our counter logs look a lot more like what we would expect

In Summary

Pretty cool huh? We now understand what a Channel, Buffered Channel, and a Directional Channel is and how to use them effectively. We also learned about Mutexes and how we can make great use of them in our Go programs to ensure that we never contaminate our state and create any race conditions.

This concludes my Learning Go series. I hope you have enjoyed reading! Although this is the end of this series, you can expect many more posts on Go in the future! In the meantime, consider subscribing to my newsletter where I announce new posts and helpful tools and tips in the software industry. I occasionally throw in a picture or two of my Golden Retrievers as well. 👋🏻

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Sign up for Best Stories

By Dev Genius

The best stories sent monthly to your email. Take a look.

By signing up, you will create a Medium account if you don’t already have one. Review our Privacy Policy for more information about our privacy practices.

Check your inbox
Medium sent you an email at to complete your subscription.

martin cartledge

Written by

https://linktr.ee/spindriftboi he/him

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

martin cartledge

Written by

https://linktr.ee/spindriftboi he/him

Dev Genius

Coding, Tutorials, News, UX, UI and much more related to development

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store