Ask a developer to discuss one of Go’s many features and there are no shortages from which to choose. Yet the catalyst for some of the most in depth discussions is often not what Go offers, but what it doesn’t. One such missing feature is the execution of one or more functions when a program exits. This blog post discusses why global exit handlers are not part of Go and how to use them anyway. Say hello…to Goodbye.

The Case Against Global Exit Handlers

A post to the Google group #golang-nuts outlines the reasons global exit handlers do not exist in Go:

  • Do not belong in multi-threaded, long-lived programs
  • May prevent the process from exiting
  • Often duplicate memory reclamation that occurs upon process exit

While the above points are all valid, when read closely, two themes emerge:

  1. Global exit handlers reclaim memory
  2. Developers could prevent the OS from reclaiming memory

First of all, global exit handlers exist in other languages and do more than just reclaim memory. Second, a feature should not be excluded just because it could be abused. There are already dozens of features in Go, if used incorrectly, that can crash or deadlock a process.

Aside from developer error, — a dubious argument at best — global exit handlers appear to be excluded due to their perceived purpose. Please continue to the next section to find out why this feature is more than just a glorified wrapper for the free function.

The Case For Global Exit Handlers

Simply put, global exit handlers do more than just reclaim memory. For example, consider an HTTP server that listens for incoming requests on a UNIX socket:

package main

import (
"fmt"
"net"
"net/http"
"os"
)

func main() {
l, err := net.Listen("unix", "http.sock")
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
http.Serve(l, http.FileServer(http.Dir(".")))
}

The above program creates the file http.sock in the working directory and serves its contents via HTTP. However, when the process terminates, the file http.sock will still exist because Go’s http package does not clean up after itself.

While it would be trivial to write some code that removes this file, what if the above code is not part of the main function? Typically there is two ways programs interact with imported packages during the shutdown process:

  1. A Close or Shutdown function
  2. A cancellable context.Context

Both of the above methods are top-down, that is the main function must both be aware of and invoke them. A cancellable context is also asynchronous — calling the cancel function does not wait on the cancelled subroutines to complete. Thus Go’s methods for participating in a process exit event are:

  • Top down
  • Asynchronous

What if the main program is unaware of some non-memory resource that should be gracefully handled upon shutdown? Or what about when the program should wait for a subroutine to complete its cleanup before the process exits?

Hello Goodbye

The Goodbye project introduces global exit handlers to Go that:

  • Can be scheduled at different priorities
  • Are executed exactly once via sync.Once
  • Are executed when the process completes normally or because of a signal

Getting Started

To use Goodbye simply type:

$ go get github.com/thecodeteam/goodbye

This gist provides and example on how to use Goodbye. The first step is to import the following package:

import "github.com/thecodeteam/goodbye"

Next, with a new context, defer goodbye.Exit.

ctx := context.Background()
defer goodbye.Exit(ctx, -1)

Deferring goodbye.Exit executes all of the registered exit handlers when the process exits normally, and -1 indicates goodbye.ExitCode is the exit code. However, not all processes complete their execution before receiving some type of signal:

goodbye.Notify(ctx,
syscall.SIGHUP, 0,
syscall.SIGINT, 0,
syscall.SIGQUIT, 0,
syscall.SIGTERM, 0)

The function goodbye.Notify begins trapping the specified signals and should be invoked as early as possible. The signals argument accepts a series of os.Signal values. Any os.Signal value in the list may be succeeded with an integer to be used as the process’s exit code when the associated signal is received. By default the process will exit with an exit code of zero, indicating a graceful shutdown.

Note

Please note that SIGABRT is not trapped. For more information on why it is a bad idea to trap SIGABRT inside of Go programs, see Golang and SIG.

Once the goodbye.Exit is deferred and goodbye.Notify is invoked, it’s time to set up some exit handlers:

Remove the UNIX socket file

The first exit handler removes the UNIX socket file used by the HTTP server.

goodbye.RegisterWithPriority(func(ctx context.Context, sig os.Signal) {
os.RemoveAll("http.sock")
log.Println("http: removed socket file: http.sock")
}, 500)

The goodbye.RegisterWithPriority function is used to register the above handler in order to specify a low priority for the handler. This is to ensure the HTTP server is closed before its socket file is removed.

Indicate the Exit Type

The next exit handler detects whether or not the program exit is a result of a signal.

goodbye.Register(func(ctx context.Context, sig os.Signal) {
if !goodbye.IsNormalExit(sig) {
log.Printf("http: received signal: %v\n", sig)
}
})

Shutdown the HTTP Server

Even though this handler is registered last, it executes before the handler that removes the socket file due to a higher priority:

goodbye.Register(func(ctx context.Context, sig os.Signal) {
s.Shutdown(ctx)
log.Println("http: shutdown")
})

Please note that because no priority is specified, this handler has a default priority of 0. That is true for the previous handler as well. Handlers registered with the same priority level are executed in order of registration.

A Normal Exit

The following example uses the gist and saves it as main.go:

$ go run main.go
2017/10/04 15:04:14 http: serving
2017/10/04 15:04:19 http: shutdown
2017/10/04 15:04:19 http: removed socket file: http.sock

The program runs for five seconds and then completes normally, removing the socket file http.sock.

A Signal Exit

In this example the same program from above is halted using CTRL-C, sending the process a SIGINT:

$ go run main.go
2017/10/04 15:05:28 http: serving
^C2017/10/04 15:05:28 http: received signal: interrupt
2017/10/04 15:05:28 http: shutdown
2017/10/04 15:05:28 http: removed socket file: http.sock

The signal is received and the same exit handlers are executed as in the previous example.

Conclusion

The Goodbye project introduces a much needed feature to Go: global exit handlers that are executed exactly once when a process exits, no matter the reason.

  • Self-promotion there. Related:
    http://github.com/xlab/closer

  • elgselgs

    “`golang
    func Hook() {
    sigs := make(chan os.Signal, 1)
    done := make(chan bool, 1)
    signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

    go func() {
    for {
    select {
    case sig := <-sigs:
    fmt.Println(sig)
    // cleanup code here
    done <- true
    }
    }
    }()

    <-done
    fmt.Println("Bye!")
    }
    “`