(Now You're) Thinking With Channels

We've been learning Go in our lunch hours at Mergermarket for a number of months now, slowly but steadily getting a grip on the language by going through Go By Example, parts of The Go Programming Language, and other great resources. It's been fun, but nothing completely new to me.

Until we started to play with channels.

Aperture Science

Have you ever played Portal, the first person puzzle game by Valve?

The key game mechanic is, well, the portal. You shoot them from a gun. They come in two flavours. There's a blue one:

blue-closed

and an orange one:

orange-closed

When there's only one of them created they look like they do above - sort've glowing but blocked. But when they're both present...

both-open

they open, and you can go through one to come out of the other. This has many applications in the game. For instance, if you wanted to run around the same corner forever.

corner

Chase yourself down an infinite corridor.

infinite

Or just wanted to experience sky diving from the comfort of your own room.

skydive

Sadly portals aren't available in the real world. Happily Go channels are.

... and you make a neat gun

To make a channel in Go, it's simply a case of calling make with the type of channel you'd like. Say I want a channel for ints

channelForInts:= make(chan int)

To put a number into this channel the syntax looks like this:

channelForInts <- 5

And to take something out of the channel:

numberFromChannel := <- channelForInts

But saying put something in and out is a little misleading. We're not making a box, we're opening a portal!

Think of channelForInts as your portal gun. It's all set up and ready to fire a new hole in reality[1]. When you send the number through the channel, you're opening the portal at the same time.

[Image of blue glowy portal with channelForInts <- 5]

And when you receive from the channel you're opening a corresponding portal and taking what comes out of it

[Image of orange glowy portal with <- channelForInts]

But here's the trick - you're opening a portal and sending/receiving at the same time. And this is where things can get messy.

If at first you don't succeed, you fail

Let's try this out for real

package main

import "fmt"

func main() {
    channelForInts := make(chan int)
    channelForInts <- 5
    number := <- channelForInts
    fmt.Println("The number is", number)
}

Try on the Go Playground

When we run the above we get

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()

When we open the blue portal and send the 5 through it (when we run channelForInts <- 5), we haven't opened the corresponding orange portal (number := <- channelForInts on the next line). And so the 5 just bounces off the closed blue portal[2].

But it's worse than that; Go just won't take no for an answer. It will keep trying to push that 5 through the channel over and over. The code is blocking until that 5 goes through. But it can never happen, we'll never get the orange channel open at the same time.

Go is smart enough to know this has happened and so tells us - deadlock! And it's deadlocked on a chan send - the send cannot happen.

It fails the other way around too

package main

import "fmt"

func main() {
    channelForInts := make(chan int)
    number := <- channelForInts
    channelForInts <- 5
    fmt.Println("The number is", number)
}

Try on the Go Playground

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

This time we open up the orange portal and wait to get our int through it. Go waits. And it waits. And it waits... but the number will never come. Just as the program blocks when we try to send something through a 'blue' portal that doesn't have a corresponding 'orange' portal waiting to pick it up, Go will wait forever for something to come out of the channel. Deadlock! again - but this time on chan receive

Unlike Portal, we can't open two portals and then walk through them. We need to open two portals and walk through them (or send an int even) at the same time.

There's a hole in the sky

In order to do something at the same time as something else in Go, we use goroutines. In short, this means running another function at the same time as the code that it is called from. Go doesn't wait for the function to return, it just gets on with the next line of code. The other function is off roaming free - an entirely separate process.

So let's try putting one of the portal openings into a goroutine so that it can happen at the same time as the other one.

package main

import "fmt"

func main() {
    channelForInts := make(chan int)
    printIntFromChannel(channelForInts)
    channelForInts <- 5
}

func printIntFromChannel(channel chan int) {
    number := <-channel
    fmt.Println("The number is:", number)
}

Try on the Go PlayGround

This will still fail - but with a little goroutine fairydust...

package main

import "fmt"

func main() {
    channelForInts := make(chan int)
    go printIntFromChannel(channelForInts)
    channelForInts <- 5

}

func printIntFromChannel(channel chan int) {
    number := <-channel
    fmt.Println("The number is:", number)
}

Try on the Go PlayGround

The number is: 5

Even though number := <-channel and channelForInts <- 5 are blocking, it doesn't matter; they're occurring in two different processes (two different goroutines). At some point both of the goroutines - main and printIntFromChannel will reach the lines which send and receive through the channel - and that's where the magic happens. The stars align, the portals open and... the number five gets printed out.

Big whoop.

Synchronization

What's interesting about the above is that, not only did we communicate the number 5 between the two processes - we also, for one moment, managed to synchronize the execution of two independent processes. This is a powerful feature of channels in Go; it's what makes them more than just jumped up queues of data or shared arrays of memory.

Actually, talking of jumped up queues...

Rise and shine, Mr. Freeman...

At the end of another Valve game - Half Life - the protagonist Gordon Freeman is placed in a transdimensional waiting room by a creepy interdimensional entity known only to fans of the game as 'The G-Man'. Just waiting for the beginning of the next game, Half Life 2. Not ageing, not changing, not visible - completely outside of time and space until it's time for him to wake up.

Just like a buffered channel in Go.

So far we've looked at unbuffered channels - to put something through them, both portals have to be open and waiting. But we can make a channel with an internal buffer by supplying an extra argument.

bufferedIntChannel := make(chan int, 3)

would make a channel of ints with a buffer of 3. So just what is a buffer?

Think of it as a series of boxes just on the other side of the channel. Not literally on the other side - we know that the other side is just the orange portal (hopefully). Think of it as a series of boxes in a nowhere space - a transdimensional waiting room. What theses boxes let you do is send things into a channel without there being a way to get them out.

If we look at our original example

package main

import "fmt"

func main() {
    channelForInts := make(chan int)
    channelForInts <- 5
    number := <- channelForInts
    fmt.Println("The number is", number)

Try on the Go Playground

and instead of make(chan int) we make(chan int, 3)

package main

import "fmt"

func main() {
    channelForInts := make(chan int, 3)
    channelForInts <- 5
    number := <- channelForInts
    fmt.Println("The number is", number)
}

Try on the Go Playground

we can see it all works just fine!

This is because, when we send the 5 into the channel (channelForInts <- 5), the program does not block as there is space for the 5 in the buffer - one of the waiting rooms is empty, so the portal is open and the 5 flies straight in.

Then, when we get to number := <- channelForInts, the five is waiting right there in the first waiting room, ready to fly out of the open orange portal.

Some things to note here: the order is maintained in the buffer - not only are the numbers waiting, they are queuing up in this null space.

package main

import "fmt"

func main() {
    channelForInts := make(chan int, 3)
    channelForInts <- 5
    channelForInts <- 8
    firstNumber := <-channelForInts
    secondNumber := <-channelForInts
    fmt.Println("The first number is", firstNumber)
    fmt.Println("The second number is", secondNumber)
}

Try on the Go Playground

The first number is 5
The second number is 8

But when the buffer gets full, the blue portal behaves like an unbuffered channel again and any further channel sends will block.

package main

import "fmt"

func main() {
    channelForInts := make(chan int, 3)
    channelForInts <- 5
    channelForInts <- 8
    channelForInts <- 13
    channelForInts <- 21
    number := <-channelForInts
    fmt.Println("The number is", number)
}

Try on the Go Playground

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan send]:
main.main()

impacted

Sorry, a bit off theme there.

The same goes for the channel receives when the buffered channel is empty

package main

import "fmt"

func main() {
    channelForInts := make(chan int, 3)
    channelForInts <- 5
    firstNumber := <-channelForInts
    secondNumber := <-channelForInts
    fmt.Println("The first number is", firstNumber)
    fmt.Println("The second number is", secondNumber)
}

Try on the Go Playground

fatal error: all goroutines are asleep - deadlock!

goroutine 1 [chan receive]:
main.main()

That just about wraps it up for buffered channels, and for Go channels in general too.

Look at me still talking when there's science to do

As almost every post about channels in Go will tell you, they're based on C. A. R. Hoare's work on Communicating Sequential Processes.[3] This information sadly never comes with a user manual about how to apply CSP theory to Go practice. But a good starting point would be the Go slogan

Do not communicate by sharing memory; instead, share memory by communicating

While you could go about using a shared memory address to communicate between these goroutines (and Go will not only let you do it, but will also supply the tools), Go would much prefer you to use its channels to communicate over, using their properties to synchronize processes and, well, to share memory.

This was a triumph. Go and play Portal now. The ending video is below so don't play it if you want to avoid spoilers. But definitely get yourself some cake.


  1. For 'reality' read 'your whole Go program' ↩︎

  2. When I was making this post a colleague commented 'why are you writing about sending Hitler's dog through a black hole?'. I apologise to the Go Gopher for my sloppy (but improving) penmanship. ↩︎

  3. From the man who brought you the null value... ↩︎