os/signal package

Go program can hear OS signals(software interrupts) the kernel sends to the process. The common ones:
1. os.Interrupt (SIGINT): User presses Ctrl+C
2. syscall.SIGTERM: kill pid, Docker/Kubernetes stop, systemd stop

Same idea as Signal Handling in C — Go just wraps it so you wait on a channel or a Context instead of writing a C signal handler.

Older way: signal.Notify

Put signals onto a channel. You block until one arrives:

c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
s := <-c   // blocks until Ctrl+C
fmt.Println("got", s)

That works, but it does not cancel your HTTP handlers / DB queries. Those already take a Context. So Go 1.16 added NotifyContext: turn “Ctrl+C arrived” into “this Context is cancelled.”

signal.NotifyContext

func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
      
Returns
ctx: A Context that becomes cancelled on Ctrl+C (or SIGTERM). Pass this into work you want to stop: HTTP server, loops, DB calls.
stop: A function you must call when you are done listening (always defer stop) It unregisters signal handler and cancels ctx
      if it is not cancelled yet.

What does ctx.Done() mean?

You do not call Done() to cancel anything. Done() returns a channel. That channel is closed when the Context is cancelled.

// Context interface (simplified)
type Context interface {
    Done() <-chan struct{}   // closed when cancelled
    Err() error              // why it was cancelled
    // ...
}
Code What it means
<-ctx.Done() Wait until cancelled (Ctrl+C, parent cancelled, or stop()). Receive succeeds when the channel is closed.
ctx.Err() After Done fires: context.Canceled (or context.DeadlineExceeded if a timeout was set on the parent).
stop() This is the cancel function. Calling it is what makes Done fire (if a signal has not already).

Same pattern as ctx, cancel := context.WithCancel(...): stop here is that cancel.

Very simple example

Program prints “working…” until you press Ctrl+C. Then <-ctx.Done() unblocks and it exits.

package main

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

func main() {
    // ctx is cancelled when Ctrl+C arrives
    // stop unregisters the handler — always defer it
    ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
    defer stop()

    fmt.Println("working... press Ctrl+C")

    for {
        select {
        case <-ctx.Done():
            // channel closed → Context cancelled
            fmt.Println("shutting down:", ctx.Err()) // context.Canceled
            return
        case <-time.After(1 * time.Second):
            fmt.Println("working...")
        }
    }
}
$ go run main.go
working... press Ctrl+C
working...
working...
^C
shutting down: context canceled

Pass the same ctx into other work

Anything that already accepts a Context will stop when the signal arrives — you do not need a second signal channel.

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()

// HTTP: Shutdown waits until ctx is cancelled (Ctrl+C)
go server.ListenAndServe()
<-ctx.Done()
server.Shutdown(context.Background())