Table of ContentsSignals are standardized messages that an operating system can send your programs.
Take Ctrl+C
for example. When running a program from the terminal and you hit Ctrl+C
, you expect the program to end immediately.
How does that work, though? Ctrl+C
is a shortcut for the POSIX signal SIGINT
. By default, this signal causes your program to be terminated.
But this is one of those signals you can handle: You can intercept it and do whatever you please.
Handling Signals in Go
The following snippet does the bare minimum in Go to handle a signal.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| package main
import (
"fmt"
"os"
"os/signal"
)
func main() {
fmt.Println("Waiting for signal.")
// Make a buffered channel.
sigch := make(chan os.Signal, 1)
// Register the signals that you want to handle.
signal.Notify(sigch, os.Interrupt)
// Wait for the signal.
<-sigch
fmt.Println("Received interrupt. Exiting.")
}
|
If you run this program, you will see the message “Waiting for signal.”. You can then press Ctrl+C
to trigger a SIGINT. The program will print “Received interrupt. Exiting.” and exit.
Exitting Go Programs Gracefully
This signal and channel craft now open your Go programs to many possibilities.
Flushing Writes Before Exitting
Imagine your Go program is working with large files, and you want to flush all writes before the program exits.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
| package main
import (
"bufio"
"fmt"
"os"
"os/signal"
"sync"
)
func main() {
// This program creates a numbers.txt file and populates it with a lot of
// numbers, one per line. Errors omitted for brevity.
// Create a file and wrap it in a buffered writer.
f, _ := os.Create("numbers.txt")
bw := bufio.NewWriter(f)
// Close this channel to stop the number generator.
stopch := make(chan struct{})
// Use a WaitGroup to wait for the number generator to stop.
wg := sync.WaitGroup{}
wg.Add(1)
// Run the number generator in a separate Go routine.
go func() {
defer wg.Done()
L:
for i := 0; i < 1000000000; i++ {
fmt.Fprintf(bw, "%d\n", i)
select {
case <-stopch:
// The stopch channel has been closed. Break the loop.
break L
default:
}
}
}()
// Make a signal channel. Register SIGINT.
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
// In a separate Go routine, wait for the signal. On signal, close the
// stopch channel so that the number generator stops.
go func() {
<-sigch
fmt.Println("Interrupted.")
close(stopch)
}()
// Wait for the number generator to stop.
wg.Wait()
fmt.Println("Flushing.")
// Flush buffered writer. Close file.
bw.Flush()
f.Close()
fmt.Println("Exiting.")
}
|
In this program, instead of allowing the default SIGINT
behaviour, it stops the generator. The main function then flushes the buffered writer and closes the file.
Shutting Down Go HTTP Server Gracefully
Another example is where you can shut down a Go HTTP server gracefully.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
| package main
import (
"context"
"fmt"
"io"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
// Errors omitted for brevity.
// Make an HTTP server.
server := http.Server{
Addr: ":8080",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Simulate a slow HTTP response.
time.Sleep(10 * time.Second)
io.WriteString(w, "Hello")
}),
}
// Start the HTTP server in a separate Go routine.
go func() {
fmt.Println("Listening for HTTP connections.")
server.ListenAndServe()
}()
// Make a signal channel. Register SIGINT.
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
// Wait for the signal.
<-sigch
fmt.Println("Interrupted. Exiting.")
// Trigger a shutdown and allow 13 seconds to drain connections. Ignoring
// CancelFunc for brevity.
ctx, _ := context.WithTimeout(context.Background(), 13*time.Second)
server.Shutdown(ctx)
}
|
In this example, the HTTP server simulates a slow response. It takes 10 seconds before sending the response.
To try out this example, start this program and navigate to https://localhost:8080 on your web browser. Then switch back to the terminal immediately and press Ctrl+C
. You will see that the program waits until your response is served before exiting.
In case there were no pending requests, the program would exit immediately.
Cancelling In-flight Requests
As Torsten Bronger pointed out in the comments, if you want to cancel the in-flight requests instead of waiting for them to be drained, you can do so by configuring a base context for the HTTP server. You can then cancel the base context and, in turn, cancel all in-flight requests, assuming you have contexts wired up correctly in all your HTTP handlers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
| package main
import (
"context"
"net/http"
"os"
"os/signal"
"time"
)
func main() {
// Make a background context with a cancel function. Start the HTTP server
// with this as the base context.
baseCtx, baseCancel := context.WithCancel(context.Background())
server := http.Server{
// ...
BaseContext: func(l net.Listener) context.Context {
return baseCtx
},
}
go server.ListenAndServe()
// Make a signal channel. Register SIGINT.
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
// Wait for the signal.
<-sigch
// Call baseCancel to cancel all in-flight requests. This assumes that you have
// contexts wired up correctly in all your HTTP handlers.
baseCancel()
// Trigger a shutdown. Ignoring CancelFunc for brevity.
ctx, _ := context.WithTimeout(context.Background(), 3*time.Second)
server.Shutdown(ctx)
}
|
Forcing Exit on Second Interrupt
When writing Go programs for other people to run on their computers, allow the program to exit immediately after receiving a second SIGINT
.
This gives users control when they want to skip the graceful exit behaviour.
You can do this by starting up a Go routine right after receiving the first interrupt. In this Go routine, you can wait on the signal channel again and exit the program after receiving a signal.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| package main
import (
"fmt"
"os"
"os/signal"
"time"
)
func main() {
// Make a signal channel. Register SIGINT.
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt)
// Wait for the signal.
<-sigch
go func() {
fmt.Println("Interrupt again to force exit.")
// Wait for a second signal.
<-sigch
os.Exit(1)
}()
fmt.Println("Interrupted. Exiting.")
// Long clean-up code goes here.
time.Sleep(5 * time.Second)
}
|
Simplifying With Context
The signal.Notify
also comes in the context variation as signal.NotifyContext
.
When the functions of your Go program support context, you can simplify signal handling by copying your context with signal.NotifyContext
. This copy of the context is marked as done as soon as one of the signals you register arrives.
The following Go code logically brings the same behaviour as the previous example under Forcing Exit on Second Interrupt but with fewer lines of code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
| package main
import (
"context"
"fmt"
"os"
"os/signal"
"time"
)
func main() {
// Make a signal-based context. The stop function, when called, unregisters
// the signals and restores the default signal behaviour.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
// Wait for the signal.
<-ctx.Done()
stop() // After calling stop, another SIGINT will terminate the program.
fmt.Println("Interrupted. Exiting.")
// Long clean-up code goes here.
time.Sleep(5 * time.Second)
}
|
Signals You Cannot Handle
There are signals, like SIGKILL
and SIGSTOP
, that you cannot handle. SIGKILL
, for example, will terminate your program, no questions asked.
Further Reading
Go’s default behaviour for various signals is documented here: https://pkg.go.dev/os/signal
And you can learn more about all the POSIX signals here: https://man7.org/linux/man-pages/man7/signal.7.html
This post is 17th of my #100DaysToOffload challenge. Want to get involved? Find out more at 100daystooffload.com.