Illustration created for “A Journey With Go”, made from the original Go Gopher, created by Renee French.

ℹ️ This article is based on Go 1.13.

The package signal provides signal handlers and allows our Go program to interact with the incoming signals. Let’s start with the listeners before diving into the internals.

Subscription

The subscription to the signals is done via channels. Here is an example of a program that listens to any interruption signal or terminal resizing:

Each os.Signal channel listens to their own set of events. Here is a diagram with the subscription workflow of the previous example:

Go also gives the ability for a channel to stop being notified — function Stop(os.Signal) — or to ignore signals — function Ignore(...os.Signal) . Here is a short example of each:

This program cannot be interrupted by CTRL + C and will never stop since the channel stopped listening to the signal for the terminal resizing before receiving from the channel a second time. Let’s now see how the listener and process phases that handle the incoming signals are built.

gsignal

During the initialization phase, the signal spawns a goroutine that runs in a loop and act as a consumer to process the signals. This loop sleeps until it gets notified. Here is the first step:

Then, when a signal reaches the program, the signal handler delegates it to a special goroutine called gsignal . This goroutine is created with a bigger stack (32k, in order to fulfill the requirement by the different OS) that is fixed and cannot grow. Each thread (represented by M ) has an internal gsignal goroutine to handle the signals. Here is the updated diagram:

gsignal analyzes the signal to check if it processable, and wakes up the sleeping goroutine along with sending the signal to the queue:

Synchronous signals, like SIGBUS or SIGFPE , are not manageable and will be converted to panics

Then, this looping goroutine can process it. It finds first the channels that have subscribed to this event, and pushes the signal to them:

The goroutine that is looping to process signals can be visualized in the traces via the tool go tool trace :

A lock or block of gsignal would make the signal handling under troubles. It also cannot allocate memory due to its fix size. This is why it is important to have two separated goroutines in the signal processing chain: one to queue the signals as soon as they arrived, and another one to process them looping on the same queue.

We can now update the illustration of the first section with the new components: