Mat Ryer · 17 Feb 2020
Mat Ryer · 17 Feb 2020
When our Go programs are running, if someone sends an interrupt signal (when they hit Ctrl+C), it kills our program immediately.
Sometimes we might like to have finished what we were doing, or tidy up some resources before exiting.
We can acheive this in Go using a mix of the os/signal
and context
packages.
Critics of this approach say that context should be reserved for request/response things. I think this is a request and response situation. You make a request with a command and arguments in a terminal, and you wait for a response. So interrupting your tool with context fits perfectly with the pattern of using context for cancellation in an HTTP world.
We will create a new context that we will use throughout our tool. We’ll then trap the interrupt signals, and when we receive the first one we will cancel the context.
Once the context has been cancelled, we’ll rely on the natural unwinding of the program to lead to the exit of the main
function,
and therefore the end of the program.
If meanwhile we recieve another signal (a second signal), then we will abort immediately via os.Exit
.
We’ll start by creating a cancallable context.
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
Normally here you would defer
the call to cancel()
so it frees up any resources used, but we’ll do that later.
Next we’ll make a channel to catch the interrupt signal from the operating system:
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
Now we will defer not only the call to the context’s cancel()
function, but also the Stop
call that accompanies signal.Notify
:
defer func() {
signal.Stop(signalChan)
cancel()
}()
Now we are going to kick off a background goroutine that waits for either the context to be cancelled (via the deferred call we just added),
or for a signal to come through our signalChan
channel. We’ll achieve this with a select
block:
go func() {
select {
case <-signalChan: // first signal, cancel context
cancel()
case <-ctx.Done():
}
<-signalChan // second signal, hard exit
os.Exit(exitCodeInterrupt)
}()
The select
will block until the context is done, or we get a signal. If we get a signal (case <-signalChan
) then we call cancel()
; this is how we cancel the context when the user presses Ctrl+C.
Execution then falls to where we are just waiting for another signal <-signalChan
. If it receives anything on the signalChan
channel (a second signal), it calls os.Exit
which exits without waiting for the program to naturally finish.
If the program naturally exits after the first signal but before the second, this goroutine will be killed when the program exits (when the main
function returns) so there’s no need to go to any extra effort to clean it up, however dissatisfying that might feel.
Finally, we are going to call out to a run
function that is going to do the heavy lifting of our program, passing in the context.Context
(which it will respect), and any dependencies from the operating system.
run
abstraction in Why you shouldn’t use func main in Go by Mat Ryer.if err := run(ctx, os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(exitCodeErr)
}
This gives us the foundation for programs that behave nicely for developers and orchestration systems alike, with graceful shutdown and tidy-up.
package main
import (
"context"
"fmt"
"os"
"os/signal"
)
const (
exitCodeErr = 1
exitCodeInterrupt = 2
)
func main() {
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
defer func() {
signal.Stop(signalChan)
cancel()
}()
go func() {
select {
case <-signalChan: // first signal, cancel context
cancel()
case <-ctx.Done():
}
<-signalChan // second signal, hard exit
os.Exit(exitCodeInterrupt)
}()
if err := run(ctx, os.Args); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(exitCodeErr)
}
}
func run(ctx context.Context, args []string) error {
for {
select {
case <-ctx.Done():
return nil
default:
// do a piece of work
}
}
}
A lot of our blog posts come out of the technical work behind a project we're working on called Pace.
We were frustrated by communication and project management tools that interrupt your flow and overly complicated workflows turn simple tasks, hard. So we decided to build Pace.
Pace is a new minimalist project management tool for tech teams. We promote asynchronous communication by default, while allowing for those times when you really need to chat.
We shift the way work is assigned by allowing only self-assignment, creating a more empowered team and protecting the attention and focus of devs.
We're currently live and would love you to try it and share your opinions on what project management tools should and shouldn't do.
What next? Start your 14 day free trial to see if Pace is right for your team
or you can share the URL directly:
https://pace.dev/blog/2020/02/17/repond-to-ctrl-c-interrupt-signals-gracefully-with-context-in-golang-by-mat-ryer.html
Thank you, we don't do ads so we rely on you to spread the word.
If you have any questions about our tech stack, working practices, or how project management will work in Pace, tweet us any time. :)
— pace.dev (@pacedotdev) February 25, 2020
Why you shouldn't use func main in Go #Golang #Patterns
Grouper component for Svelte #Tech #Svelte
How I write HTTP services after eight years #Golang #HTTP #WebServices