atomically closing termination channels in Go

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

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.

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)
}

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)
})

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()
}

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.

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Cyril David

Cyril David

111 Followers

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