Signal multiple goroutines with close(ch)

So, this past week I learned something fun at work.
Well I learned many things, but this just ticked all the boxes of a great party trick;
✅ short
✅ unexpected
✅ makes total sense if you think about it

Do I have your attention?

The code

Let’s say you have a pool of small, long-running things that you want to stop by signalling a channel

type foo struct {
	name string
	exit chan struct{}
}

func (f foo) Run() {
	fmt.Println("Starting", f.name)
	// do stuff
	<- f.exit
	fmt.Println("Stopping", f.name)
}

and you start up a bunch of those.

exit := make(chan struct{})
a := foo{"first", exit}
b := foo{"second", exit}
c := foo{"third", exit}

go a.Run()
go b.Run()
go c.Run()

The question is, how would you stop all instances of foo?

Someone might say keep track of them and send an exit signal to each one
(eg. a.exit <- struct{}{})

This won’t work as you’d expect; the shared channel means you don’t know which instance would receive the exit signal.

Another might try to send N exit signals to the shared exit channel
(eg. for i := range pool; exit <- struct{}{})

This could work, but requires that you keep meticulous track of how many instances are currently running. In the meantime, if an instance was already shut down there would be no receiver and you could get a deadlock; or if a new instance was added, then it might be left running.

The best solution here is much simpler. Just call close(exit)!

The Go language specification mentions :

For a channel c, the built-in function close(c) records that no more values will be sent on the channel. […]

After calling close, and after any previously sent values have been received, receive operations will return the zero value for the channel’s type without blocking.

So close(ch) provides a concise way to unblock all receive operations and simultaneously provide that signal to them. It also is another reminder to “make zero values useful”.

When I discussed this with a friend, he mentioned that he’d actually use a context to handle cancellation for long-running goroutines, but that’s also what context.cancelCtx uses in the background.

So that’s it for today! I love learning this kind of small and useful tidbits; let me know if you have any other fun facts around closing channels!

Until next time, bye!

Acknowledgments

Thanks to Robert Fratto for showing me this pattern.
Thanks to Kostas Stamatakis for pointing out the context package thing.

Written on January 29, 2022