Go Examples #1 - Channels and Goroutines
Posted
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 generatecount- The number of numbers to generate in the sequenceout- 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 integersdone- 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
- The reason we need to close the channel at the end of
numberGenis 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. - 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
printNumbersone 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.



















