Skip to main content
Unix signals allow your programs to respond to system events and user actions. For example, you might want a server to gracefully shutdown when it receives a SIGTERM, or a command-line tool to stop processing when it receives a SIGINT.
What are Unix signals?Signals are software interrupts that provide a way to handle asynchronous events. Common signals include SIGINT (Ctrl+C), SIGTERM (termination request), and SIGKILL (forced termination).Learn more about Unix signals on Wikipedia.

Modern Signal Handling with Contexts

Go provides a modern, context-based approach to signal handling through the signal.NotifyContext function.
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"
)

func main() {
    // Create a context that cancels when signals arrive
    ctx, stop := signal.NotifyContext(
        context.Background(), 
        syscall.SIGINT, 
        syscall.SIGTERM,
    )
    defer stop()

    // Wait for signal
    fmt.Println("awaiting signal")
    <-ctx.Done()

    // Check what caused the cancellation
    fmt.Println()
    fmt.Println(context.Cause(ctx))
    fmt.Println("exiting")
}

How It Works

1

Create signal-aware context

signal.NotifyContext returns a context that automatically cancels when any of the specified signals are received.
ctx, stop := signal.NotifyContext(
    context.Background(),
    syscall.SIGINT,   // Ctrl+C
    syscall.SIGTERM,  // Termination request
)
defer stop()  // Clean up resources
2

Wait for cancellation

Block until the context is canceled (when a signal arrives).
fmt.Println("awaiting signal")
<-ctx.Done()  // Blocks here
3

Handle the signal

Once a signal is received, you can check which one and perform cleanup.
fmt.Println(context.Cause(ctx))  // Shows the signal
fmt.Println("exiting")

Common Signals

SIGINT

Interrupt signal - Sent when user presses Ctrl+CUse for: Graceful shutdown of interactive programs

SIGTERM

Termination signal - Polite request to terminateUse for: Clean shutdown with resource cleanup

SIGKILL

Kill signal - Cannot be caught or ignoredUse for: Force termination (you can’t handle this in Go)

SIGHUP

Hangup signal - Terminal disconnectionUse for: Reload configuration without restarting

Example: Graceful Server Shutdown

Here’s a practical example of using signals for graceful server shutdown:
func main() {
    // Create HTTP server
    server := &http.Server{Addr: ":8080"}

    // Handle signals
    ctx, stop := signal.NotifyContext(
        context.Background(),
        syscall.SIGINT,
        syscall.SIGTERM,
    )
    defer stop()

    // Start server in background
    go func() {
        if err := server.ListenAndServe(); err != http.ErrServerClosed {
            log.Fatal(err)
        }
    }()

    fmt.Println("Server started on :8080")

    // Wait for signal
    <-ctx.Done()
    fmt.Println("\nShutdown signal received")

    // Create shutdown context with timeout
    shutdownCtx, cancel := context.WithTimeout(
        context.Background(),
        5*time.Second,
    )
    defer cancel()

    // Graceful shutdown
    if err := server.Shutdown(shutdownCtx); err != nil {
        log.Fatal("Server forced to shutdown:", err)
    }

    fmt.Println("Server exited gracefully")
}
This pattern ensures that in-flight requests complete before the server shuts down, preventing data loss or corrupted responses.

Running the Example

When you run the basic signals program:
$ go run signals.go
awaiting signal
Press Ctrl+C to send a SIGINT:
^C
interrupt signal received
exiting
The ^C is displayed by your terminal when you press Ctrl+C.

Context Cause

The context.Cause function tells you why the context was canceled:
<-ctx.Done()

// For signal-triggered cancellation, this includes the signal
cause := context.Cause(ctx)
fmt.Println(cause)  // Output: "interrupt signal received"
Context-based signal handling provides several benefits:
  • Composability: Easily integrate with other context-aware code
  • Cancellation propagation: Signal cancellation flows through your entire call stack
  • Resource cleanup: Automatic cleanup with defer stop()
  • Modern idiom: Aligns with Go’s context-based patterns
The older signal.Notify API still works but requires manual channel management:
// Old way (still valid)
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
signal.NotifyContext is generally preferred for new code because it integrates better with context-based APIs.
Yes! Check the signal type after receiving:
ctx, stop := signal.NotifyContext(
    context.Background(),
    syscall.SIGINT,
    syscall.SIGTERM,
    syscall.SIGHUP,
)
defer stop()

<-ctx.Done()

// Check which signal was received
cause := context.Cause(ctx)
if sig, ok := cause.(os.Signal); ok {
    switch sig {
    case syscall.SIGHUP:
        // Reload config
    case syscall.SIGINT, syscall.SIGTERM:
        // Shutdown
    }
}

Best Practices

Always defer stop()

Clean up signal handler resources to prevent leaks
ctx, stop := signal.NotifyContext(...)
defer stop()

Set shutdown timeouts

Give cleanup a time limit to prevent hanging
ctx, cancel := context.WithTimeout(
    context.Background(),
    5*time.Second,
)
defer cancel()

Handle SIGINT and SIGTERM

These are the most common graceful shutdown signals
signal.NotifyContext(
    ctx,
    syscall.SIGINT,
    syscall.SIGTERM,
)

Log signal reception

Help with debugging and monitoring
<-ctx.Done()
log.Printf("Received signal: %v", 
    context.Cause(ctx))

Exit

Control program exit status codes

Logging

Log signal events and shutdown processes

Build docs developers (and LLMs) love