Loading...
Loading...
Marco Franssen

In this post, I'll show you how to build a Go HTTP server with graceful shutdown. This approach gives the server time to finish work before it stops—such as a database transaction or another long-running operation. We'll use what we learned in my post on concurrency, so expect channels and goroutines to be part of the solution.
When I create an HTTP server, I usually start with a command-line flag to configure its listen address and port. This is especially useful when working on multiple microservices: you can run them side by side to test their integration. Let's look at providing a listen-address on the command line, with a sensible default.
package main
import (
"flag"
"log"
"os"
)
var (
listenAddr string
)
func main() {
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
flag.Parse()
logger := log.New(os.Stdout, "http: ", log.LstdFlags)
logger.Println("Server is ready to handle requests at", listenAddr)
}This reads the command-line option -listen-addr and stores its value in listenAddr. If no value is provided, it defaults to :5000. The text server listen address is the option's description in the help output. You can manage other command-line options in the same way with the flag package.
$ go build .
$ ./gracefull-webserver
Server is ready to handle requests at :5000
$ ./gracefull-webserver --help
Usage of gracefull-webserver.exe:
-listen-addr string
server listen address (default ":5000")
$ ./gracefull-webserver --listen-addr :6000
Server is ready to handle requests at :6000Now let's look at a basic web server setup. In the example below, we create a router that responds at / with HTTP status 200.
router := http.NewServeMux() // here you could also go with third party packages to create a router
// Register your routes
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
server := &http.Server{
Addr: listenAddr,
Handler: router,
ErrorLog: logger,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
}
logger.Println("Server stopped")The final if statement starts our web server and checks for errors. For example, if the port is already in use, the server can't start on that port. The program logs the error and stops. PLEASE NOTE: Add the net/http and time packages to your imports for the code at this stage.
When we run the application, it blocks at server.ListenAndServe() until we stop the server.
$ ./gracefull-webserver
Server is ready to handle requests at :5000
CTRL+C
Server stoppedSo far, so good. However, stopping the process this way doesn't gracefully shut down the server or its open connections. If someone is receiving a response when you kill the server, that response is interrupted too. We can add code to let active requests finish, up to a maximum timeout, and stop keeping completed connections alive. We'll use a separate goroutine to intercept the shutdown signal and handle it gracefully.
First, we'll add channels so our two goroutines can communicate. If you're new to goroutines and channels, have a look at my post on concurrency in Go first.
We'll define one channel to tell the main goroutine that graceful shutdown has finished, and another to receive an OS signal telling the application to stop.
In a separate goroutine, we'll wait for an interrupt signal (Ctrl+C) on the quit channel. First, we'll print a message to let the user know the server is shutting down. Then we'll use a context to give the server 30 seconds to finish active requests. The call to server.SetKeepAlivesEnabled(false) disables keep-alive connections, and server.Shutdown(ctx) handles the graceful shutdown. That's a little friendlier than slamming the door in a consumer's face.
done := make(chan bool, 1)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
go func() {
<-quit
logger.Println("Server is shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
close(done)
}()
logger.Println("Server is ready to handle requests at", listenAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
}
<-done
logger.Println("Server stopped")Once shutdown finishes, we close the done channel to notify the main goroutine. It then continues to the final logger.Println call, reports that the shutdown sequence is complete, and exits.
Below is the complete example, combining everything we've discussed into a web server boilerplate.
package main
import (
"context"
"flag"
"log"
"net/http"
"os"
"os/signal"
"time"
)
var (
listenAddr string
)
func main() {
flag.StringVar(&listenAddr, "listen-addr", ":5000", "server listen address")
flag.Parse()
logger := log.New(os.Stdout, "http: ", log.LstdFlags)
done := make(chan bool, 1)
quit := make(chan os.Signal, 1)
signal.Notify(quit, os.Interrupt)
server := newWebserver(logger)
go gracefullShutdown(server, logger, quit, done)
logger.Println("Server is ready to handle requests at", listenAddr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
logger.Fatalf("Could not listen on %s: %v\n", listenAddr, err)
}
<-done
logger.Println("Server stopped")
}
func gracefullShutdown(server *http.Server, logger *log.Logger, quit <-chan os.Signal, done chan<- bool) {
<-quit
logger.Println("Server is shutting down...")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.SetKeepAlivesEnabled(false)
if err := server.Shutdown(ctx); err != nil {
logger.Fatalf("Could not gracefully shutdown the server: %v\n", err)
}
close(done)
}
func newWebserver(logger *log.Logger) *http.Server {
router := http.NewServeMux()
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
return &http.Server{
Addr: listenAddr,
Handler: router,
ErrorLog: logger,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 15 * time.Second,
}
}As you can see, I've also made two small refactorings: server creation and graceful shutdown now have their own functions. Eagle-eyed readers might notice that the channel parameters are receive-only or send-only within those functions. That gives us compile-time checks to prevent using a channel in the wrong direction. You can also download the boilerplate here as a starting point for your own web server.
I look forward to your feedback. Please share this post with your friends and colleagues on social media.
Marco Franssen
Build a Go API with the Elasticsearch 7 client, run a local cluster with Docker Compose, and add index creation and search to a practical demo.
Marco Franssen
Build a small Go Docker image with a multi-stage build, run it with Docker Compose, and use Traefik 1.7 to route requests and scale your local web service.
Marco Franssen
Explore implicit interfaces and type assertions in Go, with practical examples of custom error types, cleaner tests and type switches.
Marco Franssen
Learn Go test and benchmark conventions with a JSON example, then explore code coverage, Testify assertions and options for comparing performance.