How do signals work in Go (os/signal)? Why must the channel passed to signal.Notify be buffered?
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
- Q545Compare strconv with fmt for number/string conversion. What do strconv.ParseInt's base and bitSize arguments do, and how do you check for range errors?
- Q546When would you use encoding/binary, encoding/gob or protobuf for serialization? What are the byte-order and compatibility pitfalls?