LanguagesHow to Handle OS Signals in Go

How to Handle OS Signals in Go

Developer.com content and product recommendations are editorially independent. We may make money when you click on links to our partners. Learn More.

The Go programming language is not built for system level programming like C, yet there are certain features that help developers interact with the underlying operating system at a bit of a low level, such as using signals. The os/signals package helps developers to work with this feature. In fact, sometimes it is necessary to work with signals in a UNIX-like environment. This Goland programming tutorial glimpses into the concept of signals in general and shows how coders can implement them in a Go program.

Read: Best Project Management Tools for Developers

What are Signals in Go and Golang?

Signals are events generated by UNIX or Linux based operating systems. The system takes some action based upon the signal generated. For example, if we want to cancel a running program, we press CTRL+C. This sends a signal called SIGINT to a program and the underlying system takes appropriate action based upon the signal generated. Understand that the SIGINT is a name associated with a number that designates a particular signal.

Signals are actually software interrupts that can be generated programmatically. They offer a way to handle asynchronous events. Typically, signals are generated by their names, rather than their numbers, to be safe. Understand that most signals and their specific actions are defined by the operating system. They also perform their functions under the purview of the operating system. Therefore, being able to handle some events does not give a limited user a free run to do anything with the system; there are signals that, even if generated, are ignored by the operating system. It is up to the operating system to decide which signals are allowed to be handled by a program.

For example, the SIGKILL and SIGSTOP signals can neither be caught, blocked, or ignored because these signals are critical in maintaining the “sanity” of the operating system functionality. They provide the kernel and root user a way to stop a process under extreme conditions. The number associated with SIGKILL is 9.

In Linux there is a utility called kill that sends a SIGTERM signal to terminate a running program. A common way to find the list of all supported signals is by using the following commands in the terminal:

OS Signals in Go

How to use the os/signal Package in Go

The os package in Go provides a platform independent interface to work with operating system functionality, such as working with the file system, creating files and directories, and so forth. The sub package, os/signal, helps developers to work with signals specifically. There are many signals available but not all signals can be handled by a program. There are signals like SIGKILL and SIGSTOP that neither can be called, nor can be ignored, by a program. The reason is simple: they are too critical and are not allowed to be misused by a mischievous program.

There are two types of signals: synchronous and asynchronous. Synchronous signals are those that are in sync with another signal, typically a clock. A pulse generated by the clock is used to indicate to the receiver that the data has arrived. So, in synchronous signalling, both the event and the timing of the event is important. Synchronous signals are typically triggered by errors in program execution. The signals, such as SIGBUS, SIGFPE, or SIGSEGV, unless triggered by os.Process.Kill, are considered to be synchronous signals. Go programs convert synchronous signals into run-time panic. This is the default behaviour in Go.

Asynchronous signals, on the other hand, do not require it to be in sync with a clock pulse. For example, the SIGHUP signals the process that the terminal it has in its control is closed. Perhaps the most common asynchronous signal we generate, by pressing the interrupt character CTRL+C, is the SIGINT. As we generate the interrupt (by pressing CTRL+C) during program execution, the SIGINT signal is sent and the program terminates immediately.

Understand the catch here: the signal SIGINT is a signal for keyboard interrupt when CTRL+C is pressed; it actually does not terminate the program. It simply means that a specific keyboard interrupt has been generated and the handler for the interrupt is responsible to deal with it in any way it likes. Of which, the default behavior is to terminate the process. We can, however, override the default behavior and write our own handler. In fact, many signals can be overridden and Go developers can write their own handler that may define custom behavior of the signal. However, unless there is a very good reason, it is not a wise idea to change the default behavior of the signals.

As an aside, once you finish this tutorial, if you want to learn more about synchronous and asynchronous functions in go, we recommend you read the Go programming tutorial: Intro to Concurrency in Go to further your knowledge.

Read: Understanding Garbage Collection in Go

Example of Using Signals in Go and Golang

The following is a very simple program to illustrate how signals can be handled in Go:

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func handler(signal os.Signal) {
	if signal == syscall.SIGTERM {
		fmt.Println("Got kill signal. ")
		fmt.Println("Program will terminate now.")
		os.Exit(0)
	} else if signal == syscall.SIGINT {
		fmt.Println("Got CTRL+C signal")
		fmt.Println("Closing.")
		os.Exit(0)
	} else {
		fmt.Println("Ignoring signal: ", signal)
	}
}

func main() {
	sigchnl := make(chan os.Signal, 1)
	signal.Notify(sigchnl)
	exitchnl := make(chan int)

	go func() {
		for {
			s := <-sigchnl
			handler(s)
		}
	}()

	exitcode := <-exitchnl
	os.Exit(exitcode)
}

The program in our example Go code consists of following steps:

  • Since the Go signal notification sends os.Signal values through a channel, we have created the channel named sigchnl that helps receive the notifications.
  • We then need to register the given channel using signal.Notify() to receive notifications for the given channel. It then relays the incoming signal. Programmers can specify incoming signals they want to handle, such as signal.Notify(sigchnl, syscall.SIGTERM, syscall.SIGINT). If no signals are given, then all incoming signals are relayed to the specified channel. This is interesting because we can write separate handlers with separate channels to deal with separate signals.
  • Next, we implement an anonymous function that runs as a goroutine. Inside this function, we run an infinite loop to invoke the user defined handler function. The parameter for the handler function is the value designated by the os.Signal interface.
  • Note that there is another channel called exitchnl which is created to make the program wait until exit code is generated, otherwise the program will simply execute and exit. However, we can also make the program wait with the following example code instead of writing the exitchnl code:
for {
	time.Sleep(30 * time.Second)
}

Go Signal Cheat Sheet

Here is a cheatsheet of the top 15 Go signals and what they mean for quick reference. A more complete reference can be found at signal(7)-Linux Manual Page.

  • SIGHUP: ‘HUP’ = ‘hung up’. This signal is generated when a controlling process dies or hangs up detected on the controlling terminal.
  • SIGINT: When a process is interrupted from keyboard by pressing CTRL+C
  • SIGQUIT: Quit from keyboard
  • SIGILL: Illegal instruction. A synonym for SIGPWR() – power failure
  • SIGABRT: Program calls the abort() function – an emergency stop.
  • SIGBUS: Bad memory access. Attempt was made to access memory inappropriately
  • SIGFPE: FPE = Floating point exception
  • SIGKILL: The kill signal. The process was explicitly killed.
  • SIGUSR1: This signal is open for programmers to write a custom behavior.
  • SIGSEGV: Invalid memory reference. In C when we try to access memory beyond array limit, this signal is generated.
  • SIGUSR2: This signal is open for programmers to write a custom behavior.
  • SIGPIPE: This signals us open for programmers to write a custom behavior.
  • SIGALRM: Process requested a wake up call by the operating system such as by calling the alarm() function.
  • SIGTERM: A process is killed

Read: How to Handle Errors in Go

How to Handle Multiple Signals in Go</2>

We can slightly modify the above code to illustrate how to handle multiple, yet specific, signals in Go:

package main

import (
	"fmt"
	"os"
	"os/signal"
	"syscall"
)

func multiSignalHandler(signal os.Signal) {

	switch signal {
	case syscall.SIGHUP:
		fmt.Println("Signal:", signal.String())
		os.Exit(0)
	case syscall.SIGINT:
		fmt.Println("Signal:", signal.String())
		os.Exit(0)
	case syscall.SIGTERM:
		fmt.Println("Signal:", signal.String())
		os.Exit(0)
	case syscall.SIGQUIT:
		fmt.Println("Signal:", signal.String())
		os.Exit(0)
	default:
		fmt.Println("Unhandled/unknown signal")
	}
}

func main() {
	sigchnl := make(chan os.Signal, 1)
	signal.Notify(sigchnl, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) //we can add more sycalls.SIGQUIT etc.
	exitchnl := make(chan int)

	go func() {
		for {
			s := <-sigchnl
			multiSignalHandler(s)
		}
	}()

	exitcode := <-exitchnl
	os.Exit(exitcode)
}

Now, to test the code, open two terminals (in Linux). Run this program from terminal 1 with the following command:

Terminal 1: go run main.go

Go to terminal 2 and find the PID (process ID) of the running program main with the command:

$ ps -e. 

Say, the process ID is 13014. We would then want to give a specific signal to the running main program using the following kill command:

$ kill -SIGHUP 13014

This will hang up the process running in terminal 1.

Next, run main.go again and test another signal, such as kill -SIGINT 13014. Repeat this process several times until you are comfortable with it. Notice how we can generate software (keyboard – CTRL+C) interrupts programmatically without actually pressing CTRL+C.

Final Thoughts on OS Signals in Go and Golang

In C, signal handling is actually very common. It is interesting to see Go also provide features to handle some low-level code. This can be quite useful because, as developers handle signals or write their own custom handler functions, we can write some cleanup code to gracefully terminate a program, say a server, or terminate any process gracefully even if CTRL+C is pressed. These low level benefits are otherwise not possible. But, understand that Go is not built to replace C; the language C is, and will remain, supreme when it comes to system level programming.

Read more Go and Golang programming tutorials and software development guides.

Get the Free Newsletter!

Subscribe to Developer Insider for top news, trends & analysis

Latest Posts

Related Stories