Signals
signal.NotifyContext ties a context to SIGINT/SIGTERM - cancel it to propagate graceful shutdown to every goroutine that respects context cancellation.
signal.Notify registers a channel to receive OS signals. signal.NotifyContext (Go 1.16) is the idiomatic way to tie a context.Context to signal receipt - when the signal arrives, the context is cancelled and the cancellation propagates through the entire call stack.
signal.Notify delivers matching signals to the channel. Always create a buffered channel (capacity 1) so the signal is not dropped if the goroutine is not ready to receive.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
fmt.Println("running, press Ctrl+C to stop")
sig := <-sigs
fmt.Println("\nreceived signal:", sig)
}signal.NotifyContext returns a context that is cancelled when the signal arrives. Pass this context to your server or worker - they stop cleanly when cancelled.
package main
import (
"context"
"fmt"
"os/signal"
"syscall"
"time"
)
func runWorker(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("worker shutting down:", ctx.Err())
return
case <-time.After(500 * time.Millisecond):
fmt.Println("worker tick")
}
}
}
func main() {
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
go runWorker(ctx)
<-ctx.Done()
fmt.Println("signal received, waiting for worker")
time.Sleep(200 * time.Millisecond)
fmt.Println("shutdown complete")
}In production
Handle SIGTERM for graceful shutdown in containerised services -- Kubernetes sends SIGTERM before SIGKILL and waits terminationGracePeriodSeconds (default 30 s) for the process to exit cleanly. If your server ignores SIGTERM and Kubernetes sends SIGKILL, in-flight requests are abruptly dropped. Use signal.NotifyContext to tie a context to SIGTERM receipt, then pass that context to http.Server.Shutdown so it waits for in-flight requests to complete before closing. Always call stop() (the function returned by signal.NotifyContext) when you are done -- it releases the signal notification resources and re-enables the default signal behaviour so a second Ctrl+C terminates the process immediately rather than hanging. For multi-signal flows (first signal = drain, second signal = force exit), use signal.Notify with a buffered channel and handle the two signals separately in a select. Never call signal.Notify with an unbuffered channel in production -- if the goroutine is not ready to receive when the signal arrives, the delivery is skipped entirely.
Enjoyed this? Get more essays on software craft delivered to your inbox.
Subscribe free