Getty Images

Concurrent programming in Go with channels and goroutines

In this hands-on guide, you'll learn how to write faster, more efficient Go code by taking advantage of channels, the language's built-in mechanism for managing concurrent tasks.

Software programs are often evaluated by efficiency, and users expect functions to be performant. If a user applies a filter to a photo on Instagram, they want to see the filter applied immediately, not wait several seconds to see the result. In the pursuit of performance, many programming languages offer threading mechanisms to distribute work concurrently.

Here's a simple example: Imagine you want to calculate the sum of a long list of numbers. With a single program using one process, or thread, you'd have to add up all the numbers one by one. But with multiple processes running at the same time, you could distribute the task of adding up the numbers among the processes.

Using two threads, you could calculate the sum of the first half of the list in one thread and the second half in the other thread. Executing both threads at the same time -- in other words, concurrently -- could theoretically halve the amount of time it takes to calculate the sum.

Concurrent programming in Go

Although many languages support threading, which lets functions run concurrently, few programming languages do it as elegantly as Go.

In earlier programming languages such as C, functions share objects by passing around memory addresses. Although functions can share context with each other using these objects, this can easily be abused, leading to functions modifying objects in unexpected ways. If access to objects shared by concurrent code isn't limited by default, it can be difficult to avoid race conditions or memory errors in languages such as C, where the programmer must control memory access themselves.

Enter channels: an ingenious built-in mechanism for concurrent Go functions to share context. There's a great saying in the document Effective Go: "Do not communicate by sharing memory; instead, share memory by communicating."

By design, channels let only one goroutine -- a lightweight thread that runs in the Go runtime -- access an object at one time. This feature inherently eliminates race conditions. Channels not only serve as the communication mechanism for concurrent threads but also are a synchronization method, controlling the behavior of threads reading values from channels.

Walkthrough: Try out using channels in a Go program

Let's look at an example to better understand the function of goroutines and channels. You can run and modify the below program on your own in the Go Playground.

Screenshot of the code for the Go program described in this article.

The prior example shows a main function, a server function and a client function. Two channels are created in the main function on lines 9 and 10: the messages channel, which contains strings, and the done channel, which contains booleans. These channels are the communication and synchronization methods that the goroutines in our example use.

On line 13, the server is "started" by calling go server(messages), which creates a goroutine using the server function. The server function itself is simply an infinite loop that reads new messages from the messages channel and prints them out.

Because the server function has now been started as an independent thread, it runs asynchronously from the rest of the main function. On lines 16 through 18, the client method is called -- also as a goroutine -- to spawn three new threads that will send messages to the messages channel.

The client function also sends a true boolean to the done channel when it is finished sending messages. This way, the main function, which receives three values from the done channel on lines 21 through 23, cannot exit until all the client functions have finished sending messages.

As this example shows, channels hold values that are passed between goroutines. As one goroutine sends a value to a channel, another goroutine receives the value and continues running. If a goroutine is expecting a value but there are no values to receive, it waits until there is some value to receive.

In the current example, this mechanism prevents the main function from exiting before the server and client goroutines have finished. Without receiving the values in the done channel on line 21 through 23, the main function would immediately exit after creating the last client goroutine on line 18.

Tips for using Go channels and goroutines

Go channels and goroutines are a great example of the developer-friendly features built into the Go programming language. There are a wide variety of applications in programming for concurrency and channels, and goroutines are a simple but powerful way to implement them.

When using channels and goroutines, it's important to remember that the function that starts the goroutine is not waiting on the spawned goroutines by default. If the parent function exits, the spawned goroutines are also terminated.

You should also be careful of goroutines that use variables created outside the goroutine, as they are not guaranteed to remain constant. For an example regarding this gotcha with more detailed information, see Go's FAQ.

Dig Deeper on DevOps

Software Quality
App Architecture
Cloud Computing
Data Center