282

I want to capture the Ctrl+C (SIGINT) signal sent from the console and print out some partial run totals.

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Sebastián Grignoli
  • 32,444
  • 17
  • 71
  • 86

11 Answers11

339

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.

Josh Correia
  • 3,807
  • 3
  • 33
  • 50
Lily Ballard
  • 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
  • 30
    Instead 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
  • 109
    Note: 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
  • 9
    Note 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
145

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

Amin Shojaei
  • 5,451
  • 2
  • 38
  • 46
  • 1
    For 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
    Why are you using a non-blocking channel? Is this necessary? – Awn Apr 06 '17 at 16:23
  • 4
    @Barry why is the buffer size 2 instead of 1? – pdeva May 05 '17 at 05:34
  • 3
    Here'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
  • 1
    This 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
31

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)
....
adamlamar
  • 4,629
  • 2
  • 27
  • 22
  • 2
    What about os.Kill? – Awn Apr 06 '17 at 16:24
  • 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 ` and gracefully shutdown, you have to use `syscall.SIGTERM`. – adamlamar Apr 07 '17 at 21:14
  • why make it buffered size 2? (or buffered at all?) – Mike Graf May 25 '22 at 22:50
  • 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
23

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)                                                     
  }                                                                
}()    
Misha Akopov
  • 12,241
  • 27
  • 68
  • 82
gravitron
  • 3,514
  • 1
  • 20
  • 18
  • 4
    Note 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
13

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

Phani Rithvij
  • 4,030
  • 3
  • 25
  • 60
willscripted
  • 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
  • 1
    Oh 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
2

look at the example

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)

}
Josh Correia
  • 3,807
  • 3
  • 33
  • 50
dılo sürücü
  • 3,821
  • 1
  • 26
  • 28
0

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()
Ankit Arora
  • 244
  • 2
  • 4
0

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.

Hlex
  • 833
  • 8
  • 16
0

From Go 1.16, you can use signal.NotifyContext, e.g.:

ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
Paweł Prażak
  • 3,091
  • 1
  • 27
  • 42
-1

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.

Ben Aldrich
  • 81
  • 1
  • 5
  • 3
    So 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
-4

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.

nik7
  • 806
  • 3
  • 12
  • 20
user212806
  • 13
  • 2