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:
- one of the listed OS signals arrives, or
- the parent Context is already cancelled, or
- 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())