Hi Folks, it’s been a while and I remembered some handy snippets I’d used for Go synchronization a while back and figured it would be a good blog post subject. It’s pretty straightforward so this should be a light read :)

You probably know how powerful the concurrency and synchronization primitives in Go are. If not, then the upshot is that the application you’re working on might benefit from having an arbitrary number of asynchronous tasks running in the background, but how do you synchronize all of these? How do you pass data between these tasks? How do you tell certain tasks to abandon work and shut down when their work is no longer necessary? On a practical level, how does one follow the “Do not communicate by sharing memory; instead, share memory by communicating” mantra? The answer to this is (in part) channels! I’m not going to go in depth in this post, but hopefully I’ll address a couple common scenarios that resonate with you.

One of the near-ubiquitous components of APIs in Go is the context.Context type. This is frequently passed in as the first argument to functions and it “carries deadlines, cancellation signals, and other request-scoped values across API boundaries and between processes.” With this type, it’s pretty easy to kick off a bunch of goroutines and reliably terminate them when you want them to stop. Not all situations call for a Context, but it’s a pretty common pattern so I’ll take the opportunity to cover it here. Consider the following scenario:

func doFoo() {
    // You need to start an asynchronous task/worker,
    // so run a for-select loop in a separate goroutine.
    go func() {
        for {
            select {
            // ...maybe read from some channels...
            }
        }
    }()
}

This for { select {} } loop will just run forever, which could easily become a resource leak if not managed correctly. What if the caller of doFoo decides this function can stop its work? Or what if the overall service is shutting down? How will this goroutine gracefully terminate? You could implement some internal logic that checks some shared memory/state that indicates when to abandon work, but that’s the kind of thing we want to avoid. In this example we’re going to leverage context.Context (which uses channels under the hood). The first step is to pass a context to doFoo so the signature becomes:

func doFoo(ctx context.Context) {}

Now, note that we have access to this context in the goroutine via a closure, so we’re free to leverage it in the application logic of the goroutine. At its simplest, we can do something like:

func doFoo(ctx context.Context) {
    // You need to start an asynchronous task/worker,
    // so run a for-select loop in a separate goroutine.
    // Listen for the context to be canceled and return from
    // then goroutine when that happens.
    go func() {
        for {
            select {
            // ...maybe read from some other channels...
            case <- ctx.Done():
                return
            }
        }
    }()
}

From the docs:

// Done returns a channel that's closed when work done on behalf of this
// context should be canceled. Done may return nil if this context can
// never be canceled. Successive calls to Done return the same value.
// The close of the Done channel may happen asynchronously,
// after the cancel function returns.

So, Done returns a channel. Our select statement above is (as written) blocked on the channel returned by Done. There’s some subtlety around when it’s appropriate to close channels (that’s the subject of its own blog post…), but the important bit here is the fact that reading on a closed channel always succeeds and returns the zero value of the channel. So, (effectively) as soon as the channel returned by Done is closed, this read will succeed and the goroutine will return. And the context package does all of this in a way that’s thread safe.

Crucially, the caller needs to create the “correct” context when passing it in to doFoo. There’s a couple different options here. If there’s no existing context, you can create one with context.Background; this context is empty (no values) and will never be canceled. Contexts form a tree in which child contexts inherit the cancellation signals and values of their parents. Cancelling a context will cancel it’s children, but child contexts cannot cancel their parents.

Once you have a context (either passed in or created with context.Background() or context.TODO()), you’ll likely want to to create a child context that encodes your application-specific cancellation signals. If you need a context that will cancel at or after a predetermined time, you can use context.WithTimeout or context.WithDeadline. If you just want a context you can cancel from the calling function, you can use context.WithCancel. These functions accept a Context and return a Context and CancelFunc. The returned Context can be canceled automatically at some fixed time (if you opted for one of the first options), or can be canceled explicitly by calling the returned CancelFunc.

// get the background context
ctx := context.Backround()
// this context won't (necessarily) cancel on any temporal basis; only
// when the returned cancelFunc is called (the parent context is the
// Background context which is never canceled, but if the parent could
// be canceled, then that would also cancel any derived Contexts).
c, cf := context.WithCancel(ctx)
// create a context that will cancel in 5 seconds
tc, tcf := context.WithTimeout(ctx, 5 * time.Second)
// this context also cancels in 5 seconds, but note that it accepts
// an arbitrary `time.Time`, rather than a `time.Duration`
dc, dcf := context.WithDeadline(ctx, time.Now().Add(5 * time.Second))

Now the parent can call doFoo and pretty trivially cancel all the goroutines spawned with the supplied Context:

c, cf := context.WithCancel(ctx)
for i:=0; i < 5; i++ {
    doFoo(c)
}
time.Sleep(1 * time.Second) // wait for work to be completed
cf() // shut everything down

Ok, so with that all out of the way, I can show the snippets I originally had in mind for this post. There are a couple scenarios where you might tweak the structure of this for-select loop to better suite your needs, but there’s a little nuance around the patterns you should use. Nothing here is too complicated and most of it is presented in the docs, but I figure having it here might be convenient.

The first situation is a simple ticker (i.e., suppose you want to do something every few seconds). You can just use time.Ticker for this (though make sure to defer ticker.Stop() to avoid a resource leak):

func doFoo(ctx context.Context) {
    go func() {
        ticker := time.NewTicker(3 * time.Second)
        defer ticker.Stop()
        for {
            select {
            case t := <- t.C:
                // do something
            case <- ctx.Done():
                return
            }
        }
    }()
}

Similarly, suppose you want to read from a channel until the context is canceled:

func doFoo(ctx context.Context, data <-chan Data) {
    go func() {
        for {
            select {
                case d := <-data:
                    // do something
                case <- ctx.Done():
                    return
            }
        }
    }()
}

Note that this will block. Suppose you don’t want to block. If that’s the case, you can supply a default section:

func doFoo(ctx context.Context, data <-chan Data) {
    go func() {
        for {
            select {
                case d := <-data:
                    // do something
                case <- ctx.Done():
                    return
                default:
                    // hmm what to do here?
            }
        }
    }()
}

Now suppose for your use case, if you hit this default section (or perhaps some other channel case you decide implement), then it would be appropriate to break out of this for loop. You can’t just use break, because that will only break out of the select statement. However, you can label your outer for loop and then pass the name of that label to break. Here’s the example from the docs:

OuterLoop:
	for i = 0; i < n; i++ {
		for j = 0; j < m; j++ {
			switch a[i][j] {
			case nil:
				state = Error
				break OuterLoop
			case item:
				state = Found
				break OuterLoop
			}
		}
	}

Personally something rubs me the wrong way about labelling loops like this so I prefer this alternative syntax I found in this StackOverflow post:

for loop := true; loop; {
    select {
    case <-msg:
        // do your task here
    case <-ctx.Done():
        loop = false
        break
    }
}

And also just for completeness, let’s address the time.After edge case outlined in this blog post. In short, do not do:

select {
  case <-time.After(time.Second):
     // do something after 1 second.
  case <-ctx.Done():
     // do something when context is finished.
     // resources created by the time.After() will not be garbage collected
  }

Basically, the call to time.After creates a timer under the hood that never gets garbage collected on the ctx.Done path. Instead, do this:

    delay := time.NewTimer(time.Second)
    select {
    case <-delay.C:
        // do something after one second.
    case <-ctx.Done():
        // do something when context is finished and stop the timer.
        if !delay.Stop() {
            // if the timer has been stopped then read from the channel.
            <-delay.C
        }
    }

This pattern is actually suggested explicitly in the docs, in the documentation for func (t *Timer) Stop() bool:

// It returns true if the call stops the timer, false if the timer has already
// expired or been stopped.
// Stop does not close the channel, to prevent a read from the channel succeeding
// incorrectly.
//
// To ensure the channel is empty after a call to Stop, check the
// return value and drain the channel.
// For example, assuming the program has not received from t.C already:
//
// 	if !t.Stop() {
// 		<-t.C
// 	}
//
// This cannot be done concurrent to other receives from the Timer's
// channel or other calls to the Timer's Stop method.
//
// For a timer created with AfterFunc(d, f), if t.Stop returns false, then the timer
// has already expired and the function f has been started in its own goroutine;
// Stop does not wait for f to complete before returning.
// If the caller needs to know whether f is completed, it must coordinate
// with f explicitly.

Here’s an another interesting post/pattern for Timers that makes use of the Reset method. I won’t copy the documentation here, but it’s worth a read. The general idea for Reset is that you can use it like below from this StackOverflow post:

func doingSomething(listenCh <-chan string) {
    d := 1 * time.Second
    t := time.NewTimer(d)
    for {
        select {
        case s := <-listenCh:
            log.Println("Received", s)
            if !t.Stop() {
                <-t.C
            }
            t.Reset(d)
        case <-t.C:
            log.Println("Timeout")
            return
        }
    }
}

That about sums up the things I had in mind for this post. I’ve repeatedly come across these patterns and had to reread some of the associated docs and nuances, so having it here as a reference is hopefully helpful.