Go

How do signals work in Go (os/signal)? Why must the channel passed to signal.Notify be buffered?

Question 547MediumGo 1.22 to 1.25

The runtime receives OS signals and, for each signal you registered with signal.Notify(ch, sigs...), forwards it to your channel instead of doing the default action (for SIGINT and SIGTERM, the default is exiting). The runtime delivers with a non-blocking send. If nobody is receiving at that moment and the channel is unbuffered, the signal is dropped. A buffer of 1 is enough for one pending signal. go vet's sigchanyzer check flags unbuffered channels.

// Low-level form
ch := make(chan os.Signal, 1) // MUST be buffered
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(ch)

// Idiomatic form (Go 1.16+): graceful HTTP shutdown
func main() {
	ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
	defer stop()

	srv := &http.Server{Addr: ":8080"}
	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(), 10*time.Second)
	defer cancel()
	if err := srv.Shutdown(shutdownCtx); err != nil {
		log.Printf("forced shutdown: %v", err)
	}
}

Gotchas: SIGKILL and SIGSTOP cannot be caught. Kubernetes sends SIGTERM, then SIGKILL after the grace period (30s by default). On Windows, only os.Interrupt (Ctrl-C/Ctrl-Break) and console close events are delivered in practice. signal.Ignore and signal.Reset exist. Signal handlers run no user code asynchronously; everything is delivered over channels, so there are no async-signal-safety concerns in your code.

More on More Standard Library Essentials

All 16 More Standard Library Essentials questions