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