How do you implement graceful shutdown with signal.NotifyContext, and what context mistake defeats it?
Question 286MediumGo 1.22 to 1.25
signal.NotifyContext returns a context that is canceled when one of the listed signals arrives. It is the idiomatic root for a process's lifetime.
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: routes()}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatal(err)
}
}()
<-ctx.Done()
stop() // restore default behavior: a second Ctrl+C kills immediately
shutdownCtx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("forced shutdown: %v", err)
}
}
Key points:
Shutdownstops accepting new connections and waits for in-flight requests untilshutdownCtxexpires.- The shutdown context must be new, created from
Background. Deriving it from the already-canceled signal context makesShutdownreturn at once. - Mistake: setting
BaseContextto return the signal context. Every in-flight request'sr.Context()is then canceled the moment SIGTERM arrives, which aborts the requests you meant to drain. If handlers need a shutdown signal, pass a separate context and let them decide. - Background workers should receive the signal context so they stop, and
mainshould wait for them (with async.WaitGroup) before exiting.
More on Context
- Q284Can you implement your own Context type? How does the stdlib propagate cancellation from a custom parent?
- Q285How do deadlines propagate across service boundaries (e.g., gRPC), and how do you manage a time budget across multiple downstream calls?
- Q287How do you test context-aware code, including timeouts and cancellation?
- Q288Is a context safe for concurrent use? What happens if you read ctx.Done() from many goroutines, or call cancel concurrently?
- Q289How should a library expose context in its API, and how do you add context support to an existing API without breaking callers?
- Q290What does this print? (AfterFunc on already-canceled and live contexts)