For the past eight years I’ve been mainly writing websites in PHP. I like PHP a lot and I find it very easy to write. But recently I’ve been looking around for different programming languages to learn with the goal of broadening my horizon and ultimately enhancing my programming skills and knowledge. After searching around a little I chose to learn Go. Since coming from a dynamic programming language I want to challenge myself learning a static, more C-like, language.

In this article I’ll try to explain concurrency in Go, one of Go’s biggest strengths. Starting with the theory of concurrency and how Go provides us solutions to use concurrency within the code we want to write.

Please consider that my knowledge of Go is still very limited and hence that some of my statements might not be correct.

What does concurrency even mean and what is its purpose?

When people hear the word concurrency they often think about parallelism. Instead of running code once, you run it on multiple instances executing at the same time, with the goal of finishing quicker or raising the amount of tasks the code can complete in the least amount of time. This is just a very basic explanation of parallelism.

Concurrency on the other hand has nothing to do with executing tasks at the same time. It’s more the structuring of tasks to run more efficiently, at the same time. Think of it as the composition of independently executing tasks. Rob Pike, a canadian software developer and contributor to Go, has a simple explanation for both terms:

“Concurrency is about dealing with a lot of things at once.”

And

“Parallelism is about doing a lot of things at once.”

Both concurrency and parallelism are related though. While concurrency is focusing on the structure, parallelism focuses on the execution. To achieve concurrency you need communication. Tasks have to be broken down into sub-tasks and their execution has to be coordinated.

Concurrency in Go

Go’s concurrency model is based on the ‘Communicating sequential processes’ (CSP) paper by Tony Hoare. CSP is all about interaction patterns in concurrent systems.

Go provides three main pillars for achieving concurrency:

Goroutine
A goroutine is similar to a thread, but it’s cheaper to execute. It runs independently in the same address space as other goroutines.

go func() // go is used to create a goroutine

Following code runs a simple program. Firstly a go routine is started, which executes the function shout. Shout is a function which receives a string and loops three times. Each time it sleeps for 100ms and then prints the string in uppercase letters. After the go routine is created the code jumps to a normal execution of the shout function. It is important to notice that Go doesn’t wait for the execution of a goroutine to finish. Otherwise concurrency wouldn’t even be possible.

package main

import (
"fmt"
"strings"
"time"
)

func shout(s string) {
for i := 0; i < 3; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(strings.ToUpper(s))
}
}

func main() {
go shout("world") // goroutine
shout("hello") // does not wait for goroutine to complete
}

The code will create different outputs the more often you run it, because the goroutine basically runs “simultaniously” next to the normal execution. For example:

Execution A:
WORLD
HELLO
HELLO
WORLD
WORLD
HELLO

Execution B:
HELLO
WORLD
WORLD
HELLO
WORLD
HELLO

If we would just run the function shout as normal executions.

func main() {
shout("world")
shout("hello")
}

The outcome would always be the same.

Execution:
WORLD
WORLD
WORLD
HELLO
HELLO
HELLO

Channels
Channels offer the functionality of passing data between goroutines. They function as communication pipes in both ways. Let’s say a goroutine has some data it wants to pass to another goroutine. To pass the data a channel is used. Go includes channels as native types.

c := make(chan type) // creates a channel of typec := make(chan string) // creates a channel of type stringc <- "some string" // pushed string into channeldata := <-c // name receives data pushed into channel

Following code snippet creates three “shouters” which can be compared to normal workers, which perform some kind of action. A shouter has a shout method which reads data from a given channel. If a name is passed to this channel the shouter shouts out the name.

package main

import (
"fmt"
"math/rand"
"strings"
"time"
)
// shouter has an id
type Shouter struct {
id int
}
// channel data is passed to name.
func (s *Shouter) shout(c chan string) {
for {
name := <-c
fmt.Printf("Shouter[%d] shouts for %s\n", s.id, strings.ToUpper(name))
}
}
func main() {
// three different names that can get shouted out
names := [3]string{"Mike", "Sara", "Toby"}
// create channel
c := make(chan string)

// create three Shouters and start goroutines for each.
for i := 0; i < 3; i++ {
shouter := &Shouter{i}
go shouter.shout(c)
}
// keep passing one of the names into the channel and timeout for 50ms
for {
c <- names[rand.Intn(3 - 0)]
time.Sleep(time.Millisecond * 50)
}
}

This code will provide an endless output looking like this:

Shouter[2] shouts for TOBY
Shouter[0] shouts for MIKE
Shouter[1] shouts for TOBY
Shouter[2] shouts for TOBY
Shouter[0] shouts for SARA
Shouter[1] shouts for MIKE
Shouter[2] shouts for SARA
Shouter[0] shouts for TOBY
Shouter[1] shouts for SARA

As one can see, each time a name gets pushed into the channel, one of the created workers picks it up and prints it. We do not know which worker will receive the name, but we know that only one worker receives one name at a time.

Select
Go offers a select statement which feels like a switch, a switch with a twist. Instead of deciding the outcome on equal values like a switch would do. The outcome is determined by listening to channels whether they have the ability to communicate.

Let’s modify our shouters a little bit. After each shout a shouter has to take a deep breath before he can shout again. So he has to wait half a second before he can shout out the next name. I’ve also modified our for loop, passing names into the channel. Passing the data is now a condition in a select statement and the default behaviour, if no channel is available, is printing “dropped”.

package main

import (
"fmt"
"math/rand"
"strings"
"time"
)

type Shouter struct {
id int
}

func (s *Shouter) shout(c chan string) {
for {
name := <-c
fmt.Printf("Shouter[%d] shouts for %s\n", s.id, strings.ToUpper(name))
time.Sleep(time.Millisecond * 500)
}
}

func main() {
names := [3]string{"Mike", "Sara", "Toby"}

c := make(chan string)

for i := 0; i < 3; i++ {
shouter := &Shouter{i}
go shouter.shout(c)
}

for {
select {
case c <- names[rand.Intn(3 - 0)]:

default:
fmt.Println("dropped")
}
time.Sleep(time.Millisecond * 50)
}
}

This snippet will print:

Shouter[0] shouts for MIKE
Shouter[1] shouts for TOBY
Shouter[2] shouts for TOBY
dropped
dropped
dropped
dropped
dropped
dropped
dropped
Shouter[0] shouts for SARA
Shouter[1] shouts for MIKE
Shouter[2] shouts for TOBY
dropped
dropped
dropped
dropped

because every 50ms we want to push a name into the channel to get shouted out. But our workers can’t handle the amount of names passed into the channel. So the channel can’t find a free shouter to shout a name and therefor is not available, resulting in a “dropped”. Select is more powerful than in this given example. Usually it’s used to decide action between multiple channels.

This was a short and simple introduction in Concurrency with Go with a small example of what one can do with it. It’s a very interesting and powerful topic. To dive fully into Concurrency you can have a look at Concurrency Patterns with Go

Sources:

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