Signal Handling Fundamentals
At the core of graceful shutdown is the ability to detect and respond to termination signals. Before diving into complex implementations, let’s establish a solid understanding of signal handling in Go.
Understanding OS Signals
Operating systems communicate with processes through signals. The most common signals relevant to application lifecycle management include:
- SIGINT (Ctrl+C): Interrupt signal, typically sent when a user presses Ctrl+C
- SIGTERM: Termination signal, the standard way to request graceful termination
- SIGKILL: Kill signal, forces immediate termination (cannot be caught or ignored)
- SIGHUP: Hangup signal, traditionally used to indicate a controlling terminal has closed
In Go, we can capture and handle these signals using the os/signal
package and channels:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// Create a channel to receive OS signals
sigs := make(chan os.Signal, 1)
// Register for specific signals
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// Create a channel to indicate when processing is done
done := make(chan bool, 1)
// Start a goroutine to handle signals
go func() {
// Block until a signal is received
sig := <-sigs
fmt.Printf("Received signal: %s\n", sig)
// Perform cleanup operations
fmt.Println("Starting graceful shutdown...")
time.Sleep(2 * time.Second) // Simulate cleanup work
fmt.Println("Cleanup completed, shutting down...")
// Signal completion
done <- true
}()
fmt.Println("Application running... Press Ctrl+C to terminate")
// Block until done signal is received
<-done
fmt.Println("Application stopped")
}
This simple example demonstrates the basic pattern for signal handling in Go:
- Create a channel to receive signals
- Register for specific signals using
signal.Notify()
- Start a goroutine to handle signals and perform cleanup
- Block the main goroutine until cleanup is complete
Context-Based Cancellation
Go’s context
package provides a powerful mechanism for propagating cancellation signals throughout your application. This is particularly useful for graceful shutdown scenarios:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"sync"
"syscall"
"time"
)
func main() {
// Create a base context with cancellation capability
ctx, cancel := context.WithCancel(context.Background())
// Create a channel to receive OS signals
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
// Create a WaitGroup to track active workers
var wg sync.WaitGroup
// Start some worker goroutines
for i := 1; i <= 3; i++ {
wg.Add(1)
go worker(ctx, i, &wg)
}
// Handle signals
go func() {
sig := <-sigs
fmt.Printf("\nReceived signal: %s\n", sig)
fmt.Println("Cancelling context...")
cancel() // This will propagate cancellation to all workers
}()
fmt.Println("Application running with workers... Press Ctrl+C to terminate")
// Wait for all workers to finish
wg.Wait()
fmt.Println("All workers have completed, shutting down...")
}
func worker(ctx context.Context, id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d starting\n", id)
// Simulate work with context awareness
for {
select {
case <-time.After(time.Second):
fmt.Printf("Worker %d performing task\n", id)
case <-ctx.Done():
fmt.Printf("Worker %d received cancellation signal, cleaning up...\n", id)
// Simulate cleanup work
time.Sleep(time.Duration(id) * 500 * time.Millisecond)
fmt.Printf("Worker %d cleanup complete\n", id)
return
}
}
}
This example demonstrates how to use context cancellation to coordinate shutdown across multiple goroutines:
- Create a cancellable context
- Pass the context to all workers
- When a termination signal is received, call
cancel()
to notify all workers - Use a
WaitGroup
to ensure all workers complete their cleanup before the application exits