How does a Go program catch Ctrl-C (SIGINT) instead of exiting immediately?
answer
- the terminal talks to the process group
- one standard-library package owns this
- you hand the runtime a place to put it
- os/signal, a buffered os.Signal channel
- or the context-flavoured wrapper
basics
~20 sCall signal.Notify with a buffered os.Signal channel listing os.Interrupt. Go then delivers Ctrl-C on that channel instead of terminating the process, so your code decides what happens next. signal.NotifyContext does the same registration and cancels a context.
solid answer
~40 sBy default a Go program that receives SIGINT just dies, because the runtime's own handler applies the default action. You opt out by registering interest: `ch := make(chan os.Signal, 1)` then `signal.Notify(ch, os.Interrupt, syscall.SIGTERM)`. From that point the runtime stops terminating the process for those signals and instead sends the `os.Signal` value on your channel; a receive on `ch` unblocks when the user presses Ctrl-C, and you run whatever shutdown work you want. The channel must be buffered, because package signal never blocks when delivering. The modern one-liner is `ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)` with `defer stop()`: it does the same registration but hands you a context that is cancelled on the first matching signal, which composes with everything that already takes a `context.Context`.
code
go · 5 linesch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
sig := <-ch // unblocks on Ctrl-C or a supervisor's SIGTERM
fmt.Println("received:", sig) // prints: received: interruptgo deeper
Be able to write the three lines from memory: make a buffered chan os.Signal, call signal.Notify with os.Interrupt, then receive from it. Know that without this the process just dies on Ctrl-C.
Explain what changes in the runtime when Notify is called, why the channel must be buffered, and how signal.NotifyContext expresses the same thing as a cancellable context.Context.
Show where registration belongs in a real program: early in main, before work starts, with the cleanup path explicit. Be ready to say what is still not guaranteed, since SIGKILL cannot be caught.
Frame it as a contract with whatever supervises the process: what your service promises to finish after a signal, and how that promise degrades when the supervisor escalates. Keep the policy in one place, not scattered per subsystem.
## What happens with no handler at all When you press Ctrl-C in a terminal, the terminal driver sends SIGINT to every process in the foreground process group. A Go binary that has registered nothing for SIGINT dies at that point. This is not the kernel's default disposition acting directly: Go's runtime installs its own handler for most signals at startup, and for SIGINT, SIGTERM and SIGHUP that handler's default action is to terminate the program as if the signal had been unhandled. The practical consequence is the same either way -- the process stops, and nothing you wrote gets a chance to run. ## Registering interest with signal.Notify The `os/signal` package is how you take over. Its central function is: ``` func Notify(c chan<- os.Signal, sig ...os.Signal) ``` After `Notify`, the package relays each of the named signals to `c`, and the runtime stops applying the default termination for those signals. If you pass no signals at all, every signal the package can catch is relayed to `c` -- almost never what you want, because it also hoovers up signals like SIGURG that the runtime uses internally. The canonical shape is three lines: ``` ch := make(chan os.Signal, 1) signal.Notify(ch, os.Interrupt, syscall.SIGTERM) sig := <-ch ``` Three details matter. First, the channel is buffered. Package signal performs a non-blocking send; if nobody is receiving and the buffer is full, the signal is discarded rather than queued, so an unbuffered channel can silently swallow a Ctrl-C. Second, you list the signals you actually care about: `os.Interrupt` for a human at a terminal, `syscall.SIGTERM` for a supervisor or init system asking the process to stop. Third, the value you receive is an `os.Signal`, whose `String()` gives you `"interrupt"` or `"terminated"` -- useful in a log line so the next person can see which one arrived. The signal arrives on an ordinary goroutine through an ordinary channel, not inside an OS signal handler. That is a genuinely nice property of Go's design: there is no async-signal-safety restriction on what you may do. You can allocate, take a mutex, log, write to a file, or call an HTTP shutdown method. In C that same code would be undefined behaviour inside a `sigaction` handler. ## signal.NotifyContext Since Go 1.16 the standard library offers a context-shaped wrapper: ``` func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) ``` It performs the same registration internally, but instead of handing you a channel it hands you a derived context that is cancelled the first time one of the listed signals arrives. `<-ctx.Done()` replaces `<-ch`, and `ctx.Err()` will be `context.Canceled`. Because almost every long-running standard-library and application API already accepts a `context.Context`, this version usually needs no plumbing of its own. The returned `stop` function unregisters the signal handling again -- like `signal.Reset` -- and should be deferred, both to release the internal goroutine and channel and to hand the signals back to their default behaviour once your program no longer wants them. ## Which signals you can and cannot catch SIGKILL and SIGSTOP can never be delivered to a handler; registering them with `signal.Notify` compiles fine and simply never fires. `os.Kill` exists as a value you can *send* to another process, not one you can receive. Anything that depends on cleanup running is therefore always best-effort: a supervisor that escalates from SIGTERM to SIGKILL will win. ## Multiple listeners `Notify` may be called many times, with different channels, for the same signal. Every registered channel receives its own copy of each delivery -- there is no single "the handler" that a later call replaces. That is what lets a library register for SIGHUP for config reload while `main` separately registers for SIGINT and SIGTERM. ## The mistakes that show up in review The first is registering the signal and then letting `main` return, or blocking on the wrong thing, so nothing ever receives -- the program becomes uninterruptible instead of graceful. The second is assuming that catching the signal makes deferred functions run: it does not do that by itself, it only gives your code the chance to run the cleanup path deliberately. The third is calling `Notify` inside a goroutine that starts *after* the moment a signal might arrive; register early, in `main`, before you start doing work a user might interrupt.
- What does signal.NotifyContext give you that signal.Notify does not?It returns a derived context cancelled on the first matching signal, plus a stop function that unregisters the handling again. Since most long-running APIs already take a context.Context, that cancellation propagates without you plumbing a channel through every layer. Under the hood it is still signal.Notify on a channel it owns.
- Can two different channels be registered for the same signal in one program?Yes. signal.Notify can be called repeatedly with different channels, and every registered channel gets its own copy of each delivery. A later call does not replace an earlier one, which is what lets a library watch SIGHUP while main separately watches os.Interrupt and syscall.SIGTERM.
- Is there any restriction on what you may do when the signal value arrives?No. The value is delivered on a normal goroutine over a normal channel, not inside an OS signal handler, so async-signal-safety rules do not apply. You may allocate, log, take locks and make blocking calls -- all of which would be undefined behaviour inside a C sigaction handler.
Without signal.Notify, Ctrl-C is a fire alarm wired straight to the building's power cut. Registering a channel puts a receptionist in front of it: the alarm now rings on your desk, and you decide what to do about it.
saying these in an interview costs you the question
- Thinks catching a signal makes deferred functions run automatically
- Passes an unbuffered channel to signal.Notify
- Believes signal.Notify can catch SIGKILL if you register it
- Registers the signal only after the long-running work has started
- Assumes a later signal.Notify call replaces the earlier registration