For example, given this code, which I am reviewing right now, (pardon me the simplification a bit, but it should convey the gist):
ctx, cancel := context.WithCancel(ctx)
sigCh := make(chan os.Signal, 1)
// Get notified of interrupts
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
// Consume signal notifications in goroutine
go func() {
// Wait for signal
<-sigCh
// Cancel the root context
cancel()
os.Exit(0)
}()
bootCh := make(chan struct{})
// Start a new goroutine
go func() {
// Wait for signal
<- bootCh
// Follow bootCh
if err := monitorBootCh(); err != nil {
errCh <- err
}
}()
I am used to actually follow this pattern:
go func() {
// Either send to channel or wait for context cancellation
select {
case ch <- processTask(task):
case <-ctx.Done():
}
}()
with a context, to cleanly shutdown any goroutines in any case.
However, I wonder if the above cases can represent any leak at all. The first routine is waiting for the SIGINT
channel itself. Therefore, if the process is stopped, the go routine shouldn't leak? Apart from that, it actually runs os.Exit()
, explicitly wiping the process?
The second case might be leaking, so maybe the context pattern is actually required there.
However, I am also generally thinking, what actually goroutine leaking is. Won't a process which is interrupted or crashed, just be wiped with all its subroutines?
As I am reviewing this code, I am wondering if I should enforce the pattern?
All goroutines of a process will be destroyed before the process exits. Goroutines cannot live beyond the process lifetime.
The second pattern you mention may not work as you expect:
go func() {
select {
case ch <- processTask(task): // processTask will be evaluated before select
case <-ctx.Done():
}
}()
Since you are sending the result of processTask
via a channel, processTask
will run first to completion. When it is done and its result is obtained, select
will be evaluated and only then ch
and ctx.Done()
will be checked for send and receive respectivelty.