I want to capture the Ctrl+C (SIGINT
) signal sent from the console and print out some partial run totals.
Is it possible to capture a Ctrl+C signal (SIGINT) and run a cleanup function, in a "defer" fashion?

- 3,807
- 3
- 33
- 50

- 32,444
- 17
- 71
- 86
11 Answers
You can use the os/signal package to handle incoming signals. Ctrl+C is SIGINT, so you can use this to trap os.Interrupt
.
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func(){
for sig := range c {
// sig is a ^C, handle it
}
}()
The manner in which you cause your program to terminate and print information is entirely up to you.

- 3,807
- 3
- 33
- 50

- 182,031
- 33
- 381
- 347
-
I see you simply call Notify() instead of signal.Notify(). Is it tha same thing? – Sebastián Grignoli Jun 29 '12 at 22:00
-
@SebastiánGrignoli: Transcription error. Typing code into SO directly does not always lead to something that can actually be executed. I'll fix that. – Lily Ballard Jun 29 '12 at 22:08
-
Ok. I asked because I saw this kind of call (ommiting the prefix) everywhere on the documentation of libraries. I think they refer to calling the function from within the libraries... – Sebastián Grignoli Jun 29 '12 at 22:20
-
@SebastiánGrignoli: Inside of any package you can call other functions within that package with the unadorned name (e.g. `Notify()`). Outside of that package, you must use the package identifier (e.g. `signal.Notify()`). So if you see documentation refer to a function without a package identifier, it's referring to that function within the current package. – Lily Ballard Jun 29 '12 at 22:21
-
30Instead of `for sig := range g {`, you can also use `<-sigchan` as in this previous answer : http://stackoverflow.com/questions/8403862/do-actions-on-end-of-execution – Denys Séguret Jun 30 '12 at 07:34
-
5@dystroy: Sure, if you're actually going to terminate the program in response to the first signal. By using the loop you can catch all the signals if you happen to decide *not* to terminate the program. – Lily Ballard Jun 30 '12 at 21:31
-
109Note: you must actually build the program for this to work. If you run the program via `go run` in a console and send a SIGTERM via ^C, the signal is written into the channel and the program responds, but appears to drop out of the loop unexpectedly. This is because the SIGRERM goes to `go run` as well! (This has cause me substantial confusion!) – William Pursell Nov 17 '12 at 22:33
-
9Note that in order for the goroutine to get processor time to handle the signal, the main goroutine must call a blocking operation or call `runtime.Gosched` in an appropriate place (in your program's main loop, if it has one) – misterbee Aug 04 '13 at 15:53
-
is there anyway i could show the current line stack that the program is currently on? in my case was some io read, which i do not know which line it ended up being stuck... – James Tan Oct 18 '16 at 04:13
-
If you have a main loop anyway, you may as well just `select {}` on the channel instead of using a separate goroutine and calling mystical `runtime` functions. – Dmiters May 22 '20 at 16:42
-
This works like a charm on Go 1.19. Thank you very much! In order to send the correct signal corresponding to ^C, but your process runs in the background (`&`), you can use ```kill -2
``` after you have located the process ID using for example ```ps -ax```. ```ps -ax | grep – Ronald Wolvers Feb 03 '23 at 14:39``` -
oh wow! Comment by William Pursell saved me couple hours of debugging. Was very confused why it drops out into terminal, but continues to run in background – metalim Jul 25 '23 at 16:56
This works:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time" // or "runtime"
)
func cleanup() {
fmt.Println("cleanup")
}
func main() {
c := make(chan os.Signal)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
cleanup()
os.Exit(1)
}()
for {
fmt.Println("sleeping...")
time.Sleep(10 * time.Second) // or runtime.Gosched() or similar per @misterbee
}
}
Checkout in the Playground

- 5,451
- 2
- 38
- 46
-
1For other readers: Look at @adamonduty 's answer for an explanation as to why you want to catch os.Interrupt and syscall.SIGTERM It would have been nice to include his explanation in this answer, especially since he posted months before you. – Chris Nov 07 '16 at 16:48
-
1
-
4
-
3Here's an excerpt from the [documentation](https://golang.org/pkg/os/signal/#Notify). "Package signal will not block sending to c: the caller must ensure that c has sufficient buffer space to keep up with the expected signal rate. For a channel used for notification of just one signal value, a buffer of size 1 is sufficient." – bmdelacruz Feb 09 '18 at 21:49
-
1This code works but I have a question. If I change `os.Exit(1)` to `os.Exit(0)`, and then run `echo $?`, the exit code is 1 instead of 0. – m0ur Apr 09 '21 at 17:29
To add slightly to the other answers, if you actually want to catch SIGTERM (the default signal sent by the kill command), you can use syscall.SIGTERM
in place of os.Interrupt. Beware that the syscall interface is system-specific and might not work everywhere (e.g. on windows). But it works nicely to catch both:
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
....

- 4,629
- 2
- 27
- 22
-
2
-
3@Eclipse Great question! `os.Kill` [corresponds](https://golang.org/pkg/os/#Signal) to `syscall.Kill`, which is a signal that can be sent but not caught. Its equivalent to the command `kill -9
`. If you want to catch `kill – adamlamar Apr 07 '17 at 21:14` and gracefully shutdown, you have to use `syscall.SIGTERM`. -
-
The answer is in the [signal.Notify documentation](https://pkg.go.dev/os/signal#Notify): `Package signal will not block sending to c: the caller must ensure that c has sufficient buffer space to keep up with the expected signal rate.` An unbuffered channel may not catch any signals at all. By estimating one slot for each registered signal it is likely to catch most signals, though not guaranteed. – adamlamar May 28 '22 at 01:35
There were (at time of posting) one or two little typos in the accepted answer above, so here's the cleaned up version. In this example I'm stopping the CPU profiler when receiving Ctrl+C.
// capture ctrl+c and stop CPU profiler
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for sig := range c {
log.Printf("captured %v, stopping profiler and exiting..", sig)
pprof.StopCPUProfile()
os.Exit(1)
}
}()

- 12,241
- 27
- 68
- 82

- 3,514
- 1
- 20
- 18
-
4Note that in order for the goroutine to get processor time to handle the signal, the main goroutine must call a blocking operation or call `runtime.Gosched` in an appropriate place (in your program's main loop, if it has one) – misterbee Aug 04 '13 at 15:54
-
@misterbee: That doesn't seem to be necessary. For example, try `go build` and executing the following program locally: https://go.dev/play/p/uVda7C4bZbe. I'd think the go runtime would fairly assign each active goroutine processor time to run, and the example program seems to confirm this. Can you share what you based the comment on? – nishanthshanmugham Oct 19 '22 at 03:36
All of the above seem to work when spliced in, but gobyexample's signals page has a really clean and complete example of signal capturing. Worth adding to this list.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sigs := make(chan os.Signal, 1)
done := make(chan bool, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sigs
fmt.Println()
fmt.Println(sig)
done <- true
}()
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
Source: gobyexample.com/signals

- 4,030
- 3
- 25
- 60

- 1,438
- 1
- 15
- 25
-
ok, I realize this is ancient, but why make it a buffered channel? afaik The program is correct with unbuffered – Mike Graf May 25 '22 at 22:49
-
3@MikeGraf From the [`Notify` example code](https://pkg.go.dev/os/signal#example-Notify): "We must use a buffered channel or risk missing the signal if we're not ready to receive when the signal is sent." – willscripted Jun 02 '22 at 16:20
-
Am I correctly understanding though that with an unbuffered channel we'd be risk missing the 2nd unhandled signal? (and with a 1 buffer channel we'd risk missing the 3rd signal etc?) – Mike Graf Aug 05 '22 at 02:11
-
1Oh that's a great question, I haven't tried it. There may be some answers in the [source docs](https://go.dev/src/os/signal/doc.go). Nothing jumped out at me so it may just need a more thorough read. – willscripted Aug 06 '22 at 12:53
When we run this program it will block waiting for a signal. By typing Ctrl+C (which the terminal shows as ^C) we can send a SIGINT signal, causing the program to print interrupt and then exit.
signal. Notify registers the given channel to receive notifications of the specified signals.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
sig := make(chan os.Signal, 1)
done := make(chan bool, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
go func() {
sig := <-sig
fmt.Println()
fmt.Println(sig)
done <- true
fmt.Println("ctrl+c")
}()
fmt.Println("awaiting signal")
<-done
fmt.Println("exiting")
}
detect HTTP request cancel
package main
import (
"fmt"
"net/http"
"time"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/path", func(writer http.ResponseWriter, request *http.Request) {
time.Sleep(time.Second * 5)
select {
case <-time.After(time.Millisecond * 10):
fmt.Println("started")
return
case <-request.Context().Done():
fmt.Println("canceled")
}
})
http.ListenAndServe(":8000", mux)
}

- 3,807
- 3
- 33
- 50

- 3,821
- 1
- 26
- 28
You can have a different goroutine that detects syscall.SIGINT and syscall.SIGTERM signals and relay them to a channel using signal.Notify. You can send a hook to that goroutine using a channel and save it in a function slice. When the shutdown signal is detected on the channel, you can execute those functions in the slice. This can be used to clean up the resources, wait for running goroutines to finish, persist data, or print partial run totals.
I wrote a small and simple utility to add and run hooks at shutdown. Hope it can be of help.
https://github.com/ankit-arora/go-utils/blob/master/go-shutdown-hook/shutdown-hook.go
You can do this in a 'defer' fashion.
example for shutting down a server gracefully :
srv := &http.Server{}
go_shutdown_hook.ADD(func() {
log.Println("shutting down server")
srv.Shutdown(nil)
log.Println("shutting down server-done")
})
l, err := net.Listen("tcp", ":3090")
log.Println(srv.Serve(l))
go_shutdown_hook.Wait()

- 244
- 2
- 4
This is another version which work in case you have some tasks to cleanup. Code will leave clean up process in their method.
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
)
func main() {
_,done1:=doSomething1()
_,done2:=doSomething2()
//do main thread
println("wait for finish")
<-done1
<-done2
fmt.Print("clean up done, can exit safely")
}
func doSomething1() (error, chan bool) {
//do something
done:=make(chan bool)
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
//cleanup of something1
done<-true
}()
return nil,done
}
func doSomething2() (error, chan bool) {
//do something
done:=make(chan bool)
c := make(chan os.Signal, 2)
signal.Notify(c, os.Interrupt, syscall.SIGTERM)
go func() {
<-c
//cleanup of something2
done<-true
}()
return nil,done
}
in case you need to clean main function you need to capture signal in main thread using go func() as well.

- 833
- 8
- 16
From Go 1.16, you can use signal.NotifyContext
, e.g.:
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()

- 3,091
- 1
- 27
- 42
Death is a simple library that uses channels and a wait group to wait for shutdown signals. Once the signal has been received it will then call a close method on all of your structs that you want to cleanup.

- 81
- 1
- 5
-
3So many lines of code and an external library dependecy to do what can be done in four lines of code? (as per accepted answer) – Jay Oct 14 '16 at 06:54
-
it allows you to do the cleanup of all of them in parallel and automatically close structs if they have the standard close interface. – Ben Aldrich Oct 18 '19 at 20:25
Just for the record if somebody needs a way to handle signals on Windows.
I had a requirement to handle from program A calling program B through os/exec but program B never was able to terminate gracefully because sending signals through cmd.Process.Signal(syscall.SIGTERM)
or other signals are not supported on Windows.
I handled this problem by creating a temp file as a signal. For example, program A creates file .signal.term
and program B needs to check if that file exists on interval base. If file exists it will exit the program and handle a cleanup if needed.
I'm sure there are other ways but this did the job.

- 806
- 3
- 12
- 20

- 13
- 2