atomically closing termination channels in Go

Cyril David
2 min readMar 27, 2020

One of the most prevalent patterns when managing the lifecycle of goroutines in Go is to have a done channel. Here’s a quick example showing this in action:

done := make(chan struct{})for {
select {
case <-done:
return
case job := <-jobs:
// execute job
}
}

In fact, this is pretty much how the Done channel in context is done under the hood.

Approaches to closing this done channel

Option 1: using a mutex

If we take a look at how it’s done in the context package, it simply uses a mutex to ensure that the action of closing the channel is only done once.

Here’s the relevant snippet showing that:

c.mu.Lock()
if c.err != nil {
c.mu.Unlock()
return // already canceled
}
c.err = err
if c.done == nil {
c.done = closedchan
} else {
close(c.done)
}

We can see that it uses the presence of the err field to denote if this work was already done, and terminates early the next this is called.

Option 2: using sync.Once

I suppose the clue here is in the name. Here’s a quick example showing this in action:

var once sync.Onceonce.Do(func() {
close(done)
})

This approach works really well in cases where you really just want to have a re-entrant way of calling close from potentially multiple goroutines.

Option 3: Using select

The last approach is to use select . Here’s an example, demonstrated via a quick test with 1000 goroutines that this approach also works and is fully re-entrant.

import (
"sync"
"testing"
)
func TestClose(t *testing.T) {
done := make(chan struct{})
closer := func() {
select {
case <-done:
default:
close(done)
}
}
wg := sync.WaitGroup{}
wg.Add(10000)
for i := 0; i < 10000; i++ {
go func() {
wg.Done()
closer()
}()
}
wg.Wait()
}

Essentially, this takes advantage of the property of closed channels that you can read from them — and having this be a precondition before closing guarantees that you’re going to close this at most once.

What about channels where you communicate data?

While the techniques described above work extremely well for done channels, they may not fit as neatly when it comes to closing channels which are used to communicate actual data.

I’ll leave the discussion of that in a future article.

--

--

Cyril David

software engineer at auth0, writing code for humans, with humans.