Go Examples #1 - Channels and Goroutines

Hanging out in the #go-nuts IRC channel on irc.freenode.net, we get asked lots of questions about channels and goroutines. While I don't claim to be an expert on either of these topics, and the official resources are incredibly good, I threw together a quick example program and figured I would share it here.

Goroutines

A goroutine in Go is an independent thread of execution in your program. Your main program is executed in one goroutine, which is automatically scheduled and run by the Go runtime. If you would like to do something 'in the background', you can just spawn a new goroutine using the go keyword. I won't give an example here, just keep this in mind as we go through this example.

Channels

Channels, in my opinion, are a bit easier to understand. A channel is a data structure that has two main operations: read and write. Channels are used to communicate values and are typed accordingly. All communication is synchronous, meaning that in order for a read to occur, someone must write to the channel, and vice versa (this can change if the channels are buffered, but we're not considering that in this example). If someone tries to read from a channel, the program (more specifically the goroutine that function is running in) will block until a writer comes along.

Example

This program is a simple example that shows how one might use channels and goroutines to accomplish a task. It's a program that will print count numbers, starting at startNum. In simple terms, you might write a function to do this like so:

package main

import "fmt"

func printNumbers(start, count int) {
    for i := 0; i < count; i++ {
        fmt.Printf("%d\n", start+i)
    }
}

func main() {
    printNumbers(3, 5)
}

Indeed this program does just what you'd expect it does. It will print the numbers 3, 4, 5, 6 and 7 to standard out. But let's put a different spin on it by using goroutines and channels.

Integer producer

package main

import "fmt"

func numberGen(start, count int, out chan<- int) {
    for i := 0; i < count; i++ {
        out <- start + i
    }
    close(out)
}

This program define a new function called numberGen that takes three arguments:

  • start - The first number to generate
  • count - The number of numbers to generate in the sequence
  • out - An output channel for integers

Instead of printing the numbers directly, this function outputs the number to the channel using the <- operator. Remember that when this write happens, the function will not proceed until someone has actually read the value that's been written. The function just sits there waiting until this happens.

You might be a bit confused by the use of chan<-, but all you need to know is that is a declaration for a channel that can only be written to. Normally a channel type can either be read from or written to, but this means you can read from a channel that you're actually supposed to be writing to. This make it clear to the compiler what you're trying to do with the channel and ensures that the function doesn't try to read from a channel that should only be written to.

When the function is done producing the sequence, it calls close(out), which closes the channel and will be used as a signal to the other side that we're done generating numbers.

Integer consumer

func printNumbers(in <-chan int, done chan<- bool) {
    for num := range in {
        fmt.Printf("%d\n", num)
    }
    done <- true
}

Here we define a new version of the printNumbers function that is only responsible for printing the numbers, not generating the sequence itself; this is a nice separation of concerns. It takes two arguments:

  • in - An input channel for integers
  • done - An output channel for boolean values

The first argument should be obvious, it's going to be the other side of the channel used in numberGen and we'll be using it to receive integers. The done channel will be used to tell the main program when all of the work is done, something we'll discuss in a bit. The main loop of this function uses range to receive values from the in channel, printing each of them when they are received. Using range with channels is extremely nice, since you don't have to manually check to see if the channel has been closed or not (which is actually somewhat confusing).

When there are no more numbers to read on the input channel, printNumbers sends the value true down the done channel, indicating that it's complete and the main program can shut down.

Main program

As all Go programs need a main function to actually run, we'll define ours here.

func main() {
    numberChan := make(chan int)
    done := make(chan bool)
    go numberGen(1, 10, numberChan)
    go printNumbers(numberChan, done)

    <-done
}

First we use the make function to create the number channel, called numberChan. Then we create a channel for boolean values called done. Next we use the go keyword to run an instance of numberGen in the background, passing it the start number, the count and the channel we've created. The actual execution of this function will all be in the background, in a new goroutine, so the main program continues. Next, the program runs an instance of printNumbers in a new goroutine, passing it both the numeric and the boolean channels. Again, this happens in a new goroutine, so main continues on.

Now, if the function ended here, the program would just stop and there would be no guarantee that any numbers would ever be generated or printed. This is precisely why we have the done channel, it's a way for the consumer goroutine, the one printing the numbers, to signal to the main program that it's complete and the whole program can be shut down. When the main program gets to the last line, where it does a read on the done channel, the main program sits and waits for someone to come along and write on that channel.

Some observations

  1. The reason we need to close the channel at the end of numberGen is to signal to the reader of the channel that there will be no more values coming. We could instead tell both the writer and the reader exactly how many values to expect, but this sort of hardcoding can make code very difficult to understand. In our example, we only need to tell the generator how many values to send and the consumer (printNumbers) can just perform its work without caring how many times it will do it.
  2. If we didn't close the channel, the program would print the ten numbers to the screen and would then panic, saying that all goroutines are asleep. The runtime is able to detect that all goroutines (the printNumbers one is the only one) are waiting for something, which means the program can't possibly proceed. This is a form of deadlock, and the runtime is able to detect it for us.

Breaking it down

So first the main program goes through and spawns two new goroutines, one that generates numbers and one that prints numbers to the output. It connects the two of these goroutines together using a channel. It also gives the consumer a channel to signal when it's work is done. It then waits for this signal, and will shut down at that point. The net effect is a program that will print the numbers 1 through 10 to the output, written in a way that takes advantage of using channels and goroutines.

Although this is a bit of a contrived example, there are a number of situations where this same pattern would be useful. For example, let's say you want to fetch 100 HTML files from a remote webserver. You could write a program that goes through each file in your list and fetches them, or you can write a program that spawns some goroutines to do the fetching in parallel for you, signalling the main program when they're done.

Downloads

Both scripts are available for download. numbers.go uses a simple loop to print numbers, while numbers-chan.go uses channels and goroutines.

 

Projects

About

James N. Whitehead II is an author, computer scientist who is currently studying for his DPhil/PhD at the Oxford University Computing Laboratory. This blog is a collection of his thoughts, projects, snippets, photos and any other bits that come along.