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)

It creates a child Context of parent. That child is cancelled when:

  1. one of the listed OS signals arrives, or
  2. the parent Context is already cancelled, or
  3. you call stop()

What it returns: ctx and stop

Return Meaning
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 (almost always defer stop()). It unregisters the signal handler and cancels ctx if it is not cancelled yet.

Why call stop()? If you never stop, the process keeps catching Ctrl+C forever. After stop(), a second Ctrl+C uses the default behavior and kills the process — that is what you want during shutdown (if graceful shutdown hangs, the user can force-quit).

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())