Go Channels Tutorial
In this tutorial, we are going to be looking at how you can use channels within your Go-based applications.
Channels are pipes that link between goroutines
within your Go based
applications that allow communication and subsequently the passing of values to
and from variables.
They are incredibly handy and can help you craft incredibly high performance, highly concurrent applications in Go with minimal fuss compared to other programming languages. This was by no means a fluke, when designing the language, the core developers decided that they wanted concurrency within their language to be a first class citizen and to make it as simple to work with as possible, without going too far and not allowing developers the freedom they need to work in.
The ability to craft concurrent systems so easily is something that drew me to the language in the first place, and I have to say, it’s been an absolute delight so far.
Note - I would recommend having a look at my other tutorial on goroutines if you wish to learn more about goroutines.
Goals
By the end of this tutorial, you will:
- Have a solid understanding as to the theory behind channels
- Be able to create simple concurrent Go applications that use channels
Prerequisites
In order to complete this tutorial, you will need to have met the following prerequisites:
- You will need Go installed on your machine.
Video Tutorial
If you wish, this tutorial is available in video format.
The Theory
The idea of channels isn’t anything new, as like many of Go’s concurrency features, these concepts have been brought forward from the likes of Hoare’s Communicating Sequential Processes (1978), CSP for short, and even from the likes of Dijkstra’s guarded commands (1975).
The developers of Go, however, have made it their mission to present these concepts in a simple a fashion as possible to enable programmers to create better, more correct, highly concurrent applications.
A Simple Example
Let’s start off by seeing how we can build up a really simple example of how
this works in Go. We’ll first create a function that goes away and computes an
arbitrary, random value and passes it back to a channel variable called
values
:
package main
import (
"fmt"
"math/rand"
)
func CalculateValue(values chan int) {
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
values <- value
}
func main() {
fmt.Println("Go Channel Tutorial")
values := make(chan int)
defer close(values)
go CalculateValue(values)
value := <-values
fmt.Println(value)
}
Let’s disect what happened here. In our main()
function, we called
values := make(chan int)
, this call effectively created our new channel so
that we could subsequently use it within our CalculateValue
goroutine.
Note - We used
make
when instantiating ourvalues
channel as, like maps and slices, channels must be created before use.
After we created out channel, we then called defer close(values)
which
deferred the closing of our channel until the end of our main()
function’s
execution. This is typically considered best practice to ensure that we tidy up
after ourselves.
After our call to defer
, we go on to kick off our single goroutine:
CalculateValue(values)
passing in our newly created values
channel as its
parameter. Within our CalculateValue
function, we calculate a single random
value between 1-10, print this out and then send this value to our values
channel by calling values <- value
.
Jumping back into our main()
function, we then call value := <-values
which
receives a value from our values
channel.
Note - Notice how when we execute this program, it doesn’t immediately terminate. This is because the act of sending to and receiving from a channel are blocking. Our
main()
function blocks until it receives a value from our channel.
Upon execution of this code, you should see the output look something like this:
$ go run main.go
Go Channel Tutorial
Calculated Random Value: {} 7
7
Summary:
myChannel := make(chan int)
- creates myChannel which is a channel of typeint
channel <- value
- sends a value to a channel
value := <- channel
- receives a value from a channel
So, instantiating and using channels in your Go programs looks fairly straightforward so far, but what about in more complex scenarios?
Unbuffered Channels
Using a traditional channel
within your goroutines can sometimes lead to
issues with behavior that you may not quite be expecting. With traditional
unbuffered
channels, whenever one goroutine sends a value to this channel,
that goroutine will subsequently block until the value is received from the
channel.
Let’s see this in a real example. If we have a look at the below code, it’s very
similar to the code that we had previously. However, we have extended our
CalculateValue()
function to perform an fmt.Println
after it has sent it’s
randomly calculated value to the channel.
In our main()
function, we’ve added a second call to
go CalculateValue(valueChannel)
so we should expect 2 values sent to this
channel in very quick succession.
package main
import (
"fmt"
"math/rand"
"time"
)
func CalculateValue(c chan int) {
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
time.Sleep(1000 * time.Millisecond)
c <- value
fmt.Println("Only Executes after another goroutine performs a receive on the channel")
}
func main() {
fmt.Println("Go Channel Tutorial")
valueChannel := make(chan int)
defer close(valueChannel)
go CalculateValue(valueChannel)
go CalculateValue(valueChannel)
values := <-valueChannel
fmt.Println(values)
}
However, when you run this, you should see that only our first goroutines’ final print statement is actually executed:
go run main.go
Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
1
Only Executes after another goroutine performs a receive on the channel
The reason for this is our call to c <- value
has blocked in our second
goroutine and subsequently the main()
function concludes it’s execution before
our second goroutine
gets a chance to complete its own execution.
Buffered Channels
The way to get around this blocking behavior is to use something called a
buffered channel. These buffered channels are essentially queues of a given size
that can be used for cross-goroutine communication. In order to create a
buffered channel as opposed to an unbuffered channel, we supply a capacity
argument to our make
command:
bufferedChannel := make(chan int, 3)
By changing this to a buffered channel, our send operation, c <- value
only
blocks within our goroutines should the channel be full.
Let’s modify our existing program to use a buffered channel and have a look at
the output. Notice that I’ve added a call to time.Sleep()
at the bottom of our
main()
function in order to lazily block our main()
function enough to allow
our goroutines to complete execution.
package main
import (
"fmt"
"math/rand"
"time"
)
func CalculateValue(c chan int) {
value := rand.Intn(10)
fmt.Println("Calculated Random Value: {}", value)
time.Sleep(1000 * time.Millisecond)
c <- value
fmt.Println("This executes regardless as the send is now non-blocking")
}
func main() {
fmt.Println("Go Channel Tutorial")
valueChannel := make(chan int, 2)
defer close(valueChannel)
go CalculateValue(valueChannel)
go CalculateValue(valueChannel)
values := <-valueChannel
fmt.Println(values)
time.Sleep(1000 * time.Millisecond)
}
Now, when we execute this, we should see that our second goroutine does indeed
continue its execution regardless of the fact a second receive has not been
called in our main()
function. Thanks to the time.Sleep()
, we can clearly
see the difference between unbuffered channels and their blocking nature and our
buffered channels and their non-blocking (when not full) nature.
Go Channel Tutorial
Calculated Random Value: {} 1
Calculated Random Value: {} 7
7
This executes regardless as the send is now non-blocking
This executes regardless as the send is now non-blocking
Conclusion
So, in this fairly lengthy tutorial, we managed to learn about the various distinct types of channels within Go. We discovered the differences between both buffered and unbuffered channels and how we could use them to our advantage within our concurrent go programs.
If you enjoyed this tutorial, then please feel free to let me know in the comments section below. If you have any suggestions as to what I could do better then I would love to hear them in the comments section below!
Further Reading
If you enjoyed this article and wish to learn more about working with Concurrency in Go, then I recommend you check out our other articles on concurrency: