Using Signals With Go

What are signals?

Signals are messages that can be sent to running programs, and are often used to request that a program performs a specific behavior. The most common signal is likely the SIGINT signal, which is sent when a developer presses Ctrl+C in the terminal. This signal tells the program to stop running, and is how we terminate a running process.

There are many other signals that can be sent to a program. In most Unix-like operating systems a developer can press Ctrl+T and have the SIGINFO signal sent to a program. This requests that the process provide some additional information.

As a Go developer, we typically don’t need to worry about signals, as most of them will be handled without us doing anything. For instance, if someone sends the SIGINT signal to our program, we don’t need to write specific code to handle it - our program will terminate automatically when it receives this signal.

While it is true that we don’t NEED to handle signals, there are occasionally situations where we can monitor for signals in order to make our programs better in some way. A common example of this is doing a graceful shutdown.

Imagine that we had an HTTP server that processes incoming web requests. This is a pretty common use case, but what happens if our server receives a SIGINT while it is in the middle of handling a web request?

By default, our server will ignore the fact that it is in the middle of processing a web request and will terminate regardless. Yikes!

Alternatively, we could add code to monitor for the SIGINT signal in our application, then we could then tell our HTTP server that it needs to stop accepting new web requests and finish processing any existing web requests when we receive the SIGINT signal. This is commonly known as a graceful shutdown.

In this article we are going to take a deeper look at how signals work in Go. In a future article we will build upon everything here to learn how to put it all together to implement graceful shutdown for a Go server.

How signals work in Go

The Go standard library provides us with the os/signal package to handle signals. At its core, the os/signal package is used to do two things:

  1. Start listening for signals, either via a channel or a context.
  2. Stop listening for signals.

There are a few ways we can do this, but we will start by looking at the Notify and Stop functions.

If we want to work with signals in our program, we can use the Notify function to register a channel to receive signals. To do this we need to first create a channel that will receive the signals, then pass the channel into the Notify function. We can optionally tell it which specific signals we want to receive so that we aren’t notified of signals we don’t care about. If we don’t specify any signals our channel will receive all of them, which isn’t usually what we want.

The following program listens for the os.Interrupt signal. When it receives a signal through the channel it prints the signal that was received, then the program exits.

func main() {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)
	sig := <-ch
	fmt.Println("Received signal:", sig)
}

You can find this in the cmd/notify directory of the accompanying repository for this article.

Let’s go through this code line by line to ensure we know what is happening.

ch := make(chan os.Signal, 1)

In the first line we are creating a channel that can receive signals. It has a buffer size of 1, which means it can buffer one signal before the channel is full. The os/signal package does NOT block when sending to a channel, so we need to use a buffer to ensure we don’t miss a signals. We only care about the first signal sent to our channel, so a buffer size of 1 is all we need. If we were interested in receiving multiple signals, we would need to consider a larger buffer and how fast our code is processing signals.

Next we call signal.Notify with our channel and the signal that we want to listen for.

signal.Notify(ch, os.Interrupt)

In this particular code, we are listening for the os.Interrupt signal, which is what is sent when we press Ctrl+C in the terminal. If we were to include additional signals as arguments to the Notify function then we would receive those signals through our channel as well. If we specified no signals, we would receive all signals through our channel. If running on a unix-based OS like Mac OS this can be verified by adding syscall.SIGINFO to the list of signals and then pressing Ctrl+T in the terminal while the program is running. (Note: This may not work on Windows).

Next we listen for a signal being sent to our channel, then store it in the sig variable.

sig := <-ch

If you are familiar with channels this should be easy to follow. If you are new to channels in Go, the short version is that we use <-ch, to denote receiving a value from a channel, and the sig := code before it is telling our program to create a new variable named sig and to assign the value received from the channel to that variable. This is very similar to writing a := add(1, 2) except we aren’t calling a function, we are getting our value from a channel.

Finally, we print the signal that was received.

fmt.Println("Received signal:", sig)

At this point our program has run out of code to run, so it will terminate. It is worth noting that our progam is not specifically terminating because it received a SIGINT; if we had more code to run, our program would continue running despite the SIGINT signal because we opted to capture it and not do anything about it.

Stop receiving signals

When we call Notify and tell our program to listen for a signal, it will continue to listen for that signal until we tell it to stop. In some cases this is what we want, but in others we might only care about the first signal, after which we want to revert to default behavior. If we go back to the graceful shutdown example, we may want to listen for the first time our program receives a SIGINT signal to start the graceful shutdown, but if our program receives a second SIGINT signal we might want to shut down immediately. This can be quite helpful if our graceful shutdown is hanging or taking longer than expected and we want to force a shutdown, or if we are doing local development and don’t care about things shutting down gracefully since we aren’t dealing with real users.

There are a few ways to achieve this, but they all essentially boil down to telling our program to stop listening for a signal.

Let’s look at how the signal.Stop function works. We will do this by starting with a program that doesn’t use the Stop function so that we can verify how the program behaves, then we can add a call to the Stop function to verify that it causes our program to act how we intended.

func main() {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)
	// Wait for the signal
	<-ch

	fmt.Println("Shutting down...")
	// Simulate a slow shutdown
	time.Sleep(3 * time.Second)
	// Shutdown has completed
	fmt.Println("Shutdown complete")
}

If we run this program and press Ctrl+C, it will exit after sleeping for 3 seconds. This somewhat simulates what a slow graceful shutdown might look like. But what happens if our program receives SIGINT multiples times? We can test this by pressing Ctrl+C multiple times, and we will see that no matter how many times we send the SIGINT signal, our program always takes 3 seconds to terminate.

One way to resolve this issue is to call signal.Stop immediately after we receive the first interrupt signal.

func main() {
	ch := make(chan os.Signal, 1)
	signal.Notify(ch, os.Interrupt)
	// Wait for the signal
	<-ch
	// Stop receiving signals
	signal.Stop(ch)

	fmt.Println("Shutting down...")
	// Simulate a slow shutdown
	time.Sleep(3 * time.Second)
	// Shutdown has completed
	fmt.Println("Shutdown complete")
}

Now if we press Ctrl+C multiple times our program will exit sooner! This happens because Stop tells the signal package to no longer send the interrupt signal to our channel and to instead handle it with default behavior.

In summary, it is important to consider whether the Stop function should be called once our programs receive a signal we are listening for.

You can find this in the cmd/stop directory of the accompanying repository for this article.

Learn Web Development with Go!

Sign up for my mailing list and I'll send you a FREE sample from my course - Web Development with Go. The sample includes 19 screencasts and the first few chapters from the book.

You will also receive emails from me about Go coding techniques, upcoming courses (including FREE ones), and course discounts.

    Avatar of Jon Calhoun
    Written by
    Jon Calhoun

    Jon Calhoun is a full stack web developer who teaches about Go, web development, algorithms, and anything programming. If you haven't already, you should totally check out his Go courses.

    Previously, Jon worked at several statups including co-founding EasyPost, a shipping API used by several fortune 500 companies. Prior to that Jon worked at Google, competed at world finals in programming competitions, and has been programming since he was a child.

    More in this series

    This post is part of the series, Signals with Go.

    Spread the word

    Did you find this page helpful? Let others know about it!

    Sharing helps me continue to create both free and premium Go resources.

    Want to discuss the article?

    See something that is wrong, think this article could be improved, or just want to say thanks? I'd love to hear what you have to say!

    You can reach me via email or via twitter.

    Recent Articles All Articles Mini-Series Progress Updates Tags About Me Go Courses

    ©2024 Jonathan Calhoun. All rights reserved.