Handle OS signals in Go by creating a buffered channel, using signal.Notify to listen for specific signals like SIGINT and SIGTERM, and then blocking on that channel in a goroutine to trigger your cleanup logic. This pattern allows your application to gracefully shut down, close open connections, and finish in-flight requests before exiting.
Here is a practical example of setting up a graceful shutdown handler for a web server:
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a context that can be cancelled
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// Setup a simple HTTP server
srv := &http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello, world!"))
}),
}
// Start the server in a goroutine
go func() {
log.Println("Server starting on :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server error: %v", err)
}
}()
// Create a channel to listen for OS signals
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// Block until a signal is received
<-quit
log.Println("Shutdown signal received, initiating graceful shutdown...")
// Give the server 5 seconds to finish processing requests
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer shutdownCancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Fatalf("Server forced to shutdown: %v", err)
}
log.Println("Server exited cleanly")
}
In this pattern, signal.Notify registers the quit channel to receive SIGINT (Ctrl+C) and SIGTERM (commonly sent by Kubernetes or systemd). The main goroutine blocks on <-quit, pausing execution until one of these signals arrives. Once received, you create a timeout context and call srv.Shutdown(), which stops accepting new connections and waits for existing ones to complete.
If you are running this in a containerized environment like Kubernetes, ensure your livenessProbe and readinessProbe are configured correctly so the pod doesn't restart before the graceful shutdown completes. You can also test this locally by running the binary and pressing Ctrl+C or sending a signal via kill -TERM <pid>.
For more complex applications with multiple resources (databases, caches, external APIs), wrap the shutdown logic in a dedicated function that iterates through your resources and closes them in the correct order. Always use a timeout context during shutdown to prevent the application from hanging indefinitely if a resource fails to close.