Question
I want to handle the Ctrl+C signal (SIGINT) sent from the terminal in a Go program so I can run some cleanup logic before the program exits. For example, I would like to print partial run totals when the user interrupts the program.
How can I capture SIGINT and perform cleanup in a way that feels similar to defer?
Short Answer
By the end of this page, you will understand how Go handles OS signals, how to listen for SIGINT when the user presses Ctrl+C, and how to run cleanup code before exiting. You will also see how this differs from normal defer behavior and how developers structure graceful shutdown logic in real Go programs.
Concept
In Go, defer runs when the surrounding function returns normally. But signals like SIGINT come from the operating system, not from your code calling return. When the user presses Ctrl+C, the process receives an interrupt signal, and unless you handle it, the program usually stops immediately.
To respond to Ctrl+C, Go programs commonly use the os/signal package. This lets your program subscribe to signals such as:
os.InterruptforCtrl+Csyscall.SIGTERMfor termination requests from the OS or container platforms
Once a signal is received, your code can:
- print current totals
- close files or network connections
- stop background goroutines
- flush buffered logs
- exit cleanly
This matters because real programs often need a graceful shutdown instead of an abrupt stop. If a program exits instantly, it may lose progress, leave resources open, or fail to report useful final state.
A key idea is this:
deferis for cleanup when your function returns- signal handling is for cleanup when the operating system interrupts your process
You can combine both. A common pattern is:
Mental Model
Think of defer as cleaning up when you decide to leave a room.
Signal handling is different: someone from outside the room knocks on the door and says, you need to wrap up now.
Your program can either:
- ignore the interruption and be forced out, or
- respond politely by finishing important tasks first
So Ctrl+C handling is like having an emergency closing checklist:
- save the current totals
- turn off machines
- lock the door
- then leave
defer is still useful, but the signal handler is what starts the shutdown process.
Syntax and Examples
In Go, you usually capture Ctrl+C with os/signal.
package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
fmt.Println("Program running. Press Ctrl+C to stop.")
<-sigChan
fmt.Println("\nCaught Ctrl+C. Running cleanup...")
fmt.Println("Partial totals: 42")
}
What this does
make(chan os.Signal, 1)creates a channel that can receive a signal.signal.Notify(sigChan, os.Interrupt)tells Go to sendSIGINTevents to that channel.<-sigChanblocks until the user pressesCtrl+C.- After the signal arrives, the cleanup code runs.
Using defer together with signal handling
A more realistic pattern is to trigger shutdown and then return normally.
Step by Step Execution
Consider this example:
package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
defer signal.Stop(sigChan)
total := 0
for {
select {
case <-time.After(1 * time.Second):
total++
fmt.Println("Processed:", total)
case <-sigChan:
fmt.Println("Interrupted")
fmt.Println("Final partial total:", total)
return
}
}
}
Step-by-step
sigChanis created to receive OS signals.signal.Notify(sigChan, os.Interrupt)registers interest inCtrl+C.totalstarts at0.- The
forloop runs forever.
Real World Use Cases
Handling SIGINT is useful in many kinds of Go programs.
Command-line tools
A CLI that processes many files can print progress before stopping:
- files completed
- files skipped
- errors seen so far
Data processing scripts
A long-running script can save a checkpoint before exiting so it can resume later.
Web servers
Servers often listen for shutdown signals and then:
- stop accepting new requests
- finish in-flight requests
- close database connections
Background workers
Queue consumers and schedulers use signal handling to avoid losing work halfway through a job.
Monitoring or metrics tools
A tool collecting stats can print a final summary when interrupted manually from the terminal.
Real Codebase Usage
In real projects, signal handling is usually part of a broader shutdown design.
Common pattern: signal -> cancel -> cleanup
Developers often use:
os/signalto detect shutdowncontext.WithCancelorsignal.NotifyContextto notify goroutinesdeferto close resources in the functions that own them
Example shape:
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
defer stop()
// start workers with ctx
// when Ctrl+C happens, ctx.Done() is closed
This scales better than putting all cleanup inside one signal block.
Guarded shutdown
Real code often ensures cleanup happens once:
- close channels only once
- flush logs once
- avoid double shutdown calls
Early return pattern
When a signal arrives, many programs:
- log the reason
- print totals or metrics
- return from
main
This is simpler and safer than calling os.Exit, because os.Exit does not run deferred functions.
Common Mistakes
1. Using os.Exit() and expecting deferred functions to run
Broken example:
defer fmt.Println("cleanup")
os.Exit(1)
Problem:
os.Exit()stops the program immediately.- Deferred calls do not run.
Avoid it by:
- returning normally from
mainwhen possible - doing explicit cleanup before calling
os.Exit
2. Forgetting to register for signals
Broken example:
sigChan := make(chan os.Signal, 1)
<-sigChan
Problem:
- nothing will ever be sent to the channel unless you call
signal.Notify
Correct version:
signal.Notify(sigChan, os.Interrupt)
3. Blocking forever in a way that never checks for signals
Broken idea:
- running a tight loop or long blocking call with no shutdown path
Comparisons
| Concept | What it does | Deferred functions run? | Best use |
|---|---|---|---|
defer | Schedules cleanup when a function returns | Yes | Closing files, unlocking mutexes, stopping timers |
SIGINT handling | Reacts to Ctrl+C from the terminal | Yes, if you return normally | Graceful shutdown and partial summaries |
os.Exit() | Terminates immediately with exit code | No | Fatal exits after explicit cleanup |
panic | Stops normal flow and begins stack unwinding | Yes, unless process is killed | Unexpected program errors |
vs signal handling
Cheat Sheet
import (
"os"
"os/signal"
)
Basic pattern
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, os.Interrupt)
defer signal.Stop(sigChan)
<-sigChan
// cleanup here
Return normally if you want defer to run
defer cleanup()
return // deferred calls run
os.Exit() skips deferred calls
defer cleanup()
os.Exit(1) // cleanup does NOT run
Common graceful shutdown loop
for {
select {
case <-work:
// do work
case <-sigChan:
// print totals / cleanup
return
}
}
Good practices
FAQ
Can Go catch Ctrl+C from the terminal?
Yes. Use the os/signal package and listen for os.Interrupt.
Does defer automatically run when I press Ctrl+C?
Not by itself. You need to handle the signal and let the function return normally.
Should I use os.Exit() in a signal handler?
Usually no, because os.Exit() skips deferred cleanup. Prefer returning from main after cleanup.
How do I print partial totals before the program exits?
Store the running totals in variables, then print them when your signal channel receives os.Interrupt.
What is the difference between SIGINT and SIGTERM?
SIGINT usually comes from Ctrl+C. SIGTERM is commonly sent by the OS, containers, or process managers to request shutdown.
Can I handle more than one signal?
Yes. You can pass multiple signals to , such as and .
Mini Project
Description
Build a small Go program that simulates a long-running task and prints a partial progress summary when the user presses Ctrl+C. This demonstrates graceful interruption, signal handling, and cleanup-friendly program structure.
Goal
Create a program that counts work items over time and cleanly reports its current total when interrupted.
Requirements
- Create a loop that simulates ongoing work.
- Track a running total in a variable.
- Listen for
Ctrl+Cusingos/signal. - When interrupted, print the partial total and exit cleanly.
- Use at least one
deferstatement for normal cleanup.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.