Certainly one of the reasons why many people are attracted to the Go language is its first-class concurrency aspects. Features like communication channels, lightweight processes (goroutines), and proper scheduling of these are not only native to the language but are integrated in a tasteful manner.
If you stay around listening to community conversations for a few days there’s a good chance you’ll hear someone proudly mentioning the tenet:
Do not communicate by sharing memory; instead, share memory by communicating.
There is a blog post on the topic, and also a code walk covering it.
That model is very sensible, and being able to approach problems this way makes a significant difference when designing algorithms, but that’s not exactly news. What I address in this post is an open aspect we have today in Go related to this design: the termination of background activity.
As an example, let’s build a purposefully simplistic goroutine that sends lines across a channel:
type LineReader struct {
Ch chan string
r *bufio.Reader
}
func NewLineReader(r io.Reader) *LineReader {
lr := &LineReader{
Ch: make(chan string),
r: bufio.NewReader(r),
}
go lr.loop()
return lr
}
The type has a channel where the client can consume lines from, and an internal buffer
used to produce the lines efficiently. Then, we have a function that creates an initialized
reader, fires the reading loop, and returns. Nothing surprising there.
Now, let’s look at the loop itself:
func (lr *LineReader) loop() {
for {
line, err := lr.r.ReadSlice('n')
if err != nil {
close(lr.Ch)
return
}
lr.Ch <- string(line)
}
}
In the loop we'll grab a line from the buffer, close the channel in case of errors and stop, or otherwise send the line to the other side, perhaps blocking while the other side is busy with other activities. Should sound sane and familiar to Go developers.
There are two details related to the termination of this logic, though: first, the error information is being dropped, and then there's no way to interrupt the procedure from outside in a clean way. The error might be easily logged, of course, but what if we wanted to store it in a database, or send it over the wire, or even handle it taking in account its nature? Stopping cleanly is also a valuable feature in many circumstances, like when one is driving the logic from a test runner.
I'm not claiming this is something difficult to do, by any means. What I'm saying is that there isn't today an idiom for handling these aspects in a simple and consistent way. Or maybe there wasn't. The tomb package for Go is an experiment I'm releasing today in an attempt to address this problem.
The model is simple: a Tomb tracks whether the goroutine is alive, dying, or dead, and the death reason.
To understand that model, let's see the concept being applied to the LineReader example. As a first step, creation is tweaked to introduce Tomb support:
type LineReader struct {
Ch chan string
r *bufio.Reader
t tomb.Tomb
}
func NewLineReader(r io.Reader) *LineReader {
lr := &LineReader{
Ch: make(chan string),
r: bufio.NewReader(r),
}
go lr.loop()
return lr
}
Looks very similar. Just a new field in the struct, and the function that creates it hasn't even been touched.
Next, the loop function is modified to support tracking of errors and interruptions:
func (lr *LineReader) loop() {
defer lr.t.Done()
for {
line, err := lr.r.ReadSlice('n')
if err != nil {
close(lr.Ch)
lr.t.Kill(err)
return
}
select {
case lr.Ch <- string(line):
case <-lr.t.Dying():
close(lr.Ch)
return
}
}
}
Note a few interesting points here: first, Done is called to track the goroutine termination right before the loop function returns. Then, the previously loose error now goes into the Kill Tomb method, flagging the goroutine as dying. Finally, the channel send was tweaked so that it doesn't block in case the goroutine is dying for whatever reason.
A Tomb has both Dying and Dead channels returned by the respective methods, which are closed when the Tomb state changes accordingly. These channels enable explicit blocking until the state changes, and also to selectively unblock select statements in those cases, as done above.
With the loop modified as above, a Stop method can trivially be introduced to request the clean termination of the goroutine synchronously from outside:
func (lr *LineReader) Stop() error {
lr.t.Kill(nil)
return lr.t.Wait()
}
In this case the Kill method will put the tomb in a dying state from outside the running goroutine, and Wait will block until the goroutine terminates itself and notifies via the Done method as seen before. This procedure behaves correctly even if the goroutine was already dead or in a dying state due to internal errors, because only the first call to Kill with an actual error is recorded as the cause for the goroutine death. The nil value provided to t.Kill is used as a reason when terminating cleanly without an actual error, and it causes Wait to return nil once the goroutine terminates, flagging a clean stop per common Go idioms.
This is pretty much all that there is to it. When I started developing in Go I wondered if coming up with a good convention for this sort of problem would require more support from the language, such as some kind of goroutine state tracking in a similar way to what Erlang does with its lightweight processes, but it turns out this is mostly a matter of organizing the workflow with existing building blocks.
The tomb package and its Tomb type are a tangible representation of a good convention for goroutine termination, with familiar method names inspired in existing idioms. If you want to make use of it, go get the package with:
$ go get launchpad.net/tomb
The API documentation with details is available at:
http://gopkgdoc.appspot.com/pkg/launchpad.net/tomb
Have fun!
UPDATE 1: there was a minor simplification in the API since this post was originally written, and the post was changed accordingly.
UPDATE 2: there was a second simplification in the API since this post was originally written, and the post was changed accordingly once again to serve as reference.
Read more

Latest Official Posts