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):
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.
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, andselect
: 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.
Recommended reading
If you’re looking to sink your teeth into more Go-related topics I recommend the following:
- Get Programming with Go
- Learning Go: Context package
- Learning Go: Interface Types - Part 1
- Learning Go: Interface Types - Part 2