Gracefully Shutting Down a Server in Go
The post covers how to gracefully shut down a server in Go. This is important as there may be some tooling you're using that is batching things locally i.e. traces, logs, events, etc. behind the scenes and if the server shutdowns, we may lose this data. What we need to do is to listen to these shutdown signals and run any clean up tasks needed before we take things down.
This is a pretty common practice and definitely something you'll want to be aware of if you're using Kubernetes as pods can start up and shut down for a lot of reasons like new deployments, scaling down after a traffic spike, draining a node, etc.
Basic Server Example
The code below starts up a pretty basic server in Go.
package main
import (
"log"
"net/http"
)
func main() {
// Configure server
log.Println("configuring server ...")
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("received call")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server
log.Println("starting server ...")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Println("an error occurred running the server")
}
// Clean up
log.Println("cleaning up before shutdown")
}
When we use Control+C to kill the server, we aren't able to get to the clean up code at the bottom of main. The output looks like this:
2021/05/28 01:09:06 configuring server ...
2021/05/28 01:09:06 starting server ...
^Csignal: interrupt
Handling Signals
What we'd like to do is capture this signal and run the clean up tasks before shutting down. We can do this by creating a channel and relaying the specified signals to this channel like so:
Now we have to add some code that waits for these signals, we can do that at the bottom of the main function like this:
_ = <-signals
In order for this to work, we need to start up our server in a Go routine, so that startup of the server doesn't block up the execution. This way our server will wait on the line above to receive a signal at that channel.
The resulting code would look like this:
package main
import (
"log"
"net/http"
"os"
"os/signal"
"syscall"
)
func main() {
// Configure channels
log.Println("configuring channel ...")
signals := make(chan os.Signal, 1)
signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM)
// Configure server
log.Println("configuring server ...")
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
log.Println("received call")
})
server := &http.Server{
Addr: ":8080",
Handler: mux,
}
// Start server
go func() {
log.Println("starting server ...")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Printf("an error occurred running the server")
}
}()
// Wait for signals
_ = <-signals
log.Println("shutting down server ...")
log.Println("server shutdown")
}
Now, when we use Control+C, our server shutdown code will execute like we wanted it to:
2021/05/28 01:32:01 configuring channel ...
2021/05/28 01:32:01 configuring server ...
2021/05/28 01:32:01 starting server ...
^C2021/05/28 01:32:02 shutting down server ...
2021/05/28 01:32:02 server shutdown