What happens when a Go program ends

Maneesh Chaturvedi
4 min readJun 12, 2021

--

Ever wondered what happens when a go program ends. What happens to goroutines that might already still be running or deferred statements? Do file handles get closed? What about memory? How do you ensure a graceful shutdown? How would you handle interrupt or an abort signal? What about os.Exit?

Let’s start with the simplest case; when your program naturally falls out of the main function block. The Go program returns an exit code to its parent process when the main function exits. On Linux, this is an integer value between 0 and 125. By default, the main function, or rather os.Exit returns zero, which means everything is fine. A non-zero value means something went wrong, and you would typically call os.Exit with a non-zero value in your main function to signal that. When the program exits, the operating system takes care of any memory allocated to the program, and any open file handles associated with the program.

Filehandles are an integral concept in Linux-based systems. Everything is treated as a file in the Linux world. Even if you don’t close the file programmatically, the operating system maintains a reference counter of all the files associated with the program. It would close it and decrement the reference count. However, it is recommended to close any file handles that the program opened. The reason for this is that the program can get terminated for various reasons, some of which are not in your control. Assume that you are writing to a file and something unexpected happens which causes the program to terminate. If the program just ended in the middle of writing to disk, you would end up in a state on disk that wasn’t desirable. This leads us into thinking about graceful shutdown, where we notice that the program wants to end, or the operating system wants to end this program, but we’ve got some work to do before we terminate. So what are our options for doing something like that? How can we know that the program will end, and how can we perform some work before it ends?

One common way a program might terminate is if it receives an interrupt signal(Ctrl+C). When you hit Ctrl+C in the command line, the operating system sends a signal. Go wraps the signal in a channel. Once your program receives a signal, it needs to handle it in some way. Go uses the os/signal package to get notified about when you receive a signal. Something wants to end your program, and using the os/signal package lets you capture that and perform cleanup that needs to be done so that the program can gracefully shutdown. There is a multitude of signals. We will not talk about all of them here. The os/signal package documentation covers all the details of the different kinds of signals. Signals like SIGKILL(kill) and SIGSTOP cannot be caught, blocked, or ignored; hence, these cannot be handled gracefully. That’s what’s dangerous about kill; if you send a kill to a process, it never gets the opportunity to clean up.

So what are the ways of handling graceful shutdown?

One of the most common ways and one which is used is to pass a cancellation context. You pass in a context as the first argument through the call chain of all your programs. At your program's entry point, you check whether the context has either been closed or an error is returned. If the context has been closed or returns an error, you perform any clean-up before aborting the operation. When using this approach, you have to write signal handling code yourself. Refer to Francesc Campoy’s tutorial for detailed coverage of the context package.

Go 1.16 introduced NotifyContext in the signal package, which cancels a context on a signal. NotifyContext returns a copy of the parent context that is marked done. Previously, you had to setup up a signal handler including channel and separate Go routine yourself if you wanted to cancel some context, but the NotifyContext takes care of that now.

When do you write code to ensure graceful shutdown?

The two most common scenario’s where you would invest in writing code that handles signals and ensures graceful shutdown are

  • A command-line utility
  • Code that runs in a cloud environment or inside a container. Interrupts in such environments tell you that the instance is going away or the container will be killed. These would be ideal cases to finish in-transit work before shutting down.

Another way to get a kind of graceful shutdown, or at least of cleaning up after you, is with a defer statement. The defer keyword is used to defer the execution of functions at the point when your main function is just about to return. However, deferred functions are not always guaranteed to execute before the main function returns. For example, invoking os.Exit or pressing CTRL + C to interrupt your program will result in your deferred functions not getting called. The snippet below can be used to check the behavior of the deferred function in the presence of an os.Exit call or what happens when you press CTRL + C.

--

--

Maneesh Chaturvedi
Maneesh Chaturvedi

Written by Maneesh Chaturvedi

Seasoned Software Professional, Author, Learner for Life

No responses yet