Learning Go <- Channels
Introduction
As a Platform Engineer working exclusively with AWS day-to-day, I’ve found myself spending less and less time writing code. To remedy this, I decided to learn Go. One of the things I really like about Go is how it implements Concurrency - in particular, how Goroutines communicate using channels, known as the CSP (Communicating Sequential Processes) model.
Having recently stood up this site, writing a small, helpful post on this topic felt like a good way to kick things off. So below is a quick primer on Concurrency in Go - an intro to Goroutines, Channels, and how I remember which side the <- goes on!
Concurrency in Go
Concurrency in Go is made up of two things - Goroutines, and Channels.
Goroutines
A goroutine is effectively a lightweight thread. See below for what one looks like.
go func() {
// some work you want to execute concurrently
}()When you set a Goroutine off, it’s running concurrently to your main function. It shares the same address space (memory) as your program, but executes independently. Because it breaks the linear flow, you can’t just do return response within a Goroutine - there’s no way to return the value that it produces, and in any case, where would it return it to? Your main program has moved on after setting the Goroutine off.
Enter Channels.
Channels
A channel (chan) is a conduit through which you can send and receive values. You send and receive values using the channel operator, which is a left-pointing arrow (<-).
First off, the basics:
- There are three types of Channels - bi-directional (
chan), send-only (chan<-), and receive-only (<-chan) - You will always want to initially create (
make) a bi-directional channel by usingchan. - You use the
channeloperator (<-) to make it send-only or receive-only in the context of how you want a function to use it. - The
channeloperator, i.e. the arrow, is always a left-facing arrow (<-), whether it’s a send-only or a receive-only.
Now you know that, the only remaining question becomes - when do you use chan, chan<-, or <-chan? The answer, as alluded to above, comes down to restricting how you want a function to use that channel. It’s like the Principle of Least Privilege, but for Go Concurrency.
NOTE:: Even though above I describe a channel as a conduit, I found that the best way to visualize a channel is as a pipe.
this pipe is a channel
---------------------------------
---------------------------------Bi-directional
As mentioned above, a channel can be bi-directional, simply by being passed as a chan. This means that the channel is two way.
You can send things through it:
someBiDirectionalChannel := make(chan int)
go func() {
defer close(someBiDirectionalChannel)
someBiDirectionalChannel <- 123 // send 123 to someBiDirectionalChannel
}()And you can receive from it:
for r := range someBiDirectionalChannel { // receive from the channel
// do stuff
}Send-only
Send-only channels can be passed to a function as chan<-.
Let’s visualise this using the “pipe” analogy:
this pipe is a channel
---------------------------------
<- // data flows in to the pipe
---------------------------------The arrow being at the end is the only position where it makes sense that data can be flowing in to the pipe. Therefore, chan<- is a send-only channel.
Below is an example where the produce function uses a send-only channel:
// produce accepts a send-only channel (chan<-)
func produce(pings chan<- string) {
pings <- "hello world"
// If you tried to do `msg := <-pings `
// The compiler would throw an error: "invalid operation: <-pings (receive from send-only type chan<- string)"
}
func main() {
messages := make(chan string)
// Go automatically converts the bidirectional channel to send-only for the function
go produce(messages)
}Receive-only
Receive-only channels are written as <-chan.
Again, let’s visualise this using the “pipe” analogy:
this pipe is a channel
---------------------------------
<- // data
---------------------------------The arrow being at the end is the only position where it makes sense that data can be flowing out of the pipe. It is being received by whatever is at the end of the pipeline, and therefore <-chan is a receive-only channel.
Below is an example where the consume function uses a receive-only channel:
// consume accepts a receive-only channel (chan<-)
func consume(pings <-chan string) {
// We can only read from this channel
msg := <-pings
// If you tried to do `pings <- "some data"`
// The compiler would error: "invalid operation: pings <- "some data" (send to receive-only type <-chan string)"
}
func main() {
comms := make(chan string)
// from the previous example, `produce` sends data to the channel
go produce(comms)
// Pass the same channel as receive-only
consume(comms)
}Conclusion
And that’s it - that’s your very quick primer to using Goroutines and Channels in Go.