In the following example, when Ctrl+C is pressed, the work gorountine finishes gracefully before main exits:
package main
import (
"os"
"time"
"os/signal"
"context"
"syscall"
"fmt"
)
func work(ctx context.Context, done chan struct{}) {
defer close(done)
fmt.Println("work starting its loop")
out:
for {
select {
case <-ctx.Done():
fmt.Println("ctx.Done() inside work")
break out
default:
time.Sleep(time.Second)
fmt.Println("work heartbeat")
}
}
fmt.Println("work finished")
}
func main() {
interrupt := make(chan os.Signal, 1)
signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
done := make(chan struct{})
go func() {
<-interrupt
cancel()
}()
go work(ctx, done)
<-ctx.Done()
fmt.Println("ctx.Done() inside main")
<-done
fmt.Println("Main finished")
}
However, I suspect the code is suboptimal. Is the done channel necessary? Shouldn't graceful termination of goroutines be achieved with context alone? What if there were many worker goroutines, not just one?
Actually it has gotten much simpler with
signal.NotifyContext: We simply pass a context and get a "ContextWithCancel". Now, if that context is "Done" we received a signal and if we pass the context to all of our goroutines, all goroutines will take note of that and finish.