rss resume / curriculum vitae linkedin linkedin gitlab github twitter mastodon instagram
Learning Go: Introduction to Concurrency Patterns, Goroutines and Channels
Aug 12, 2021

Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I’ll earn a commission. Please notice your final price is not affected at all by using those links.

Welcome to the first post covering Concurrency in Go, in this post I will describe to you what are the primitives in Go for dealing with concurrency, specifically goroutines, channels and all the required keywords to make a good use of them.



What is Concurrency?

Concurrency is the composition of independently executing processes, in other words it’s about dealing with lots of things at once. Concurrency is usually confused with Parallelism, which is the simultaneous execution of (possibly related) computations it’s about doing lots of things at once.

A real life analogy would be a coffee shop, in the case of Concurrency we refer to using the same resources to deal with some tasks, in the Coffee Shop this will be about thinking what coffee to brew (computation) and to use the coffee machine when the decision is made (CPU):

What is Concurrency?

In the case of Parallelism we take a similar approach but instead of sharing the coffee machine each line of customers has a dedicated coffee machine therefore both lines they can brew their coffee independent of each other.

What is Parallelism?

In Go to there are two concepts used to deal with Concurrency: goroutines and channels.

What is a Goroutine?

The code used for this post is available on Github.

A Goroutine is an independently executed function that uses the go keyword, the simplest example would be something like:

func main() {
	go hello()
}

func hello() {
	fmt.Println("it's most likely you will never see this")
}

Where the function hello is executed as a goroutine in the main function. If you run this example you will notice the message will be not be printed most of the times (or at all really!), this is because main exits before hello completes its execution.

The reason of that behavior is because in the code we don’t have a way to indicate main to wait for hello to complete, to do so we would need to use another concept in Go called channels.

What is a Channel?

A Channel is a mechanism through which we can send and receive values. To create a channel in Go we use the make keyword and we indicate the type of values the channel will use, similar to the types map and slice.

When creating a Channel we can also indicate a length, indicating a length creates Buffered Channels and not indicating a length creates Unbuffered Channels. The difference between both of them relies on how those block when sending or receiving values when the capacity is reached or when there are no more elements to receive.

By default sender and receiver will block if the other side is not ready, and in the case of a Buffered Channel sending to a full one will block, and receiving from it will only block when it’s empty.

In order to work with channels we have to use a new operator <-, the arrow, the location of the arrow relative to the channel variable will indicate a receive or a send action, for example if the arrow is on the right side:

ch <- v

it indicates we are sending the value v to the channel ch, and if the arrow is on the left side:

v = <-ch

it indicates we are receiving a value from channel ch and assigning it to v.

A much more complete example looks like:

func main() {
	ch := make(chan string)

	go func() {
		fmt.Println(time.Now(), "taking a nap")

		time.Sleep(2 * time.Second)

		ch <- "hello"
	}()

	fmt.Println(time.Now(), "waiting for message")

	v := <-ch

	fmt.Println(time.Now(), "received", v)
}

Where we used a channel, ch, to receive a value, "hello", from a goroutine we launched.

There are three keywords in Go that allows us to work with channels:

  • close: indicates a channel is no longer usable for sending values,
  • range: to continuously receive values in a channel until it’s closed, and
  • select: acts similar to the switch keyword but for multiple channels.

A much more complete example looks like this:

func main() {
	ch := make(chan int, 2)
	exit := make(chan struct{})

	go func() {
		for i := 0; i < 5; i++ {
			fmt.Println(time.Now(), i, "sending")
			ch <- i
			fmt.Println(time.Now(), i, "sent")

			time.Sleep(1 * time.Second)
		}

		fmt.Println(time.Now(), "all completed, leaving")

		close(ch)
	}()

	go func() {
		for {
			select {
			case v, open := <-ch:
				if !open {
					close(exit)
					return
				}

				fmt.Println(time.Now(), "received", v)
			}
		}
	}()

	fmt.Println(time.Now(), "waiting for everything to complete")

	<-exit

	fmt.Println(time.Now(), "exiting")
}

Where the first goroutine is sending values to ch and the second goroutine is receiving those values and printing them out. The use of close is visible in the first one when after the for finishes which then will make, in the second goroutine, the value of open to become false indicating the channel ch was closed and there’s nothing else to read triggering another close, in this case the one for the exit channel which was used to wait for both goroutines to complete to finally exit the program.

Conclusion

This post kicks off the series covering Concurrency Patterns in Go, stay tuned for more.

If you’re looking to sink your teeth into more Go-related topics I recommend the following:


Back to posts