Blog.

Go webserver with graceful shutdown

MF

Marco Franssen /

7 min read1247 words

Cover Image for Go webserver with graceful shutdown

In this blogpost I want to show you how you can make a http webserver in Go with gracefull shutdown. Using this approach you allow the server to clean up some resources before it actually shuts down. Think about finishing a database transaction or some other long operation. We will be using the things we learned in my blogpost on concurency. So expect to see channels and go routines as part of the solution.

When I create new http servers I usually start with an commandline flag to provide the port number to run my server. Especially when working on multiple microservices this comes in handy so you can run your webservers next to each other, to test the integration of those. Lets have a look at how we can provide a listen-address from the commandline when we start our server, including a sane 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)
}

playground

This will read a commandline option -listen-addr and put the value in our variable listenAddr. If no value is provided it will use :5000 as default value. The text server listen address will be used as description for the help text. So all commandline options you would like to have can be managed by using 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 :6000

Now lets have a look at a basic setup of a webserver. In below example we create a new router which listens on / to respond with a http 200 status.

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")

In the last if statement we start our webserver and check for any errors. E.g. the given port could be in use and therefore not be able to start our webserver on the given port. If this happens it will log the error and stop the program. PLEASE NOTE: you have to add the net/http package to your imports to make the code work at this stage.

When we now run our application you will see it will block at the line server.ListenAndServe() until you kill the process.

$ ./gracefull-webserver
Server is ready to handle requests at :5000
CTRL+C
Server stopped

So far so good and all working fine. However this would not gracefully shut down the server and any potential open connections to your webserver. Imagine someone is receiving a response from the server at the moment you kill the server, then also that response will immediately be killed. To allow the server to finish any open request we can build in some code to gracefully handle the work in progress with a max timeout. We will also change the server to not keep any connections that finish alive. To do so we will add some more code which runs on a separate go routine to intercept the signal to shutdown the application and do some gracefull handling of that.

First thing to do is add some channels which allow us to communicate between the 2 go routines. In case this is the first time you deal with routines and channels in Go you might want to checkout my blogpost on concurrency in Go first.

First we will define one channel to inform the main routine the gracefull shutdown finished. We will also add a channel which waits for any signals from the OS to shutdown our application.

In a separate Go routine we will wait for any interrupt signal (ctrl+c) to the quit channel. First thing we will do, is to print a message on the console to inform the user the server is shutting down. Using a context we will then give the server 30 seconds to gracefully shutdown. With server.SetKeepAlivesEnabled(false) we will inform the webserver to not keep any existing connections alive which basically gives us the gracefull shutdown behavior, instead of just slapping the door in front of a consumers 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 is done we inform the main Go routine using the done channel we finished the gracefull shutdown. This results in the program to continue execution to the last logger.Println line to output the shutdown sequence fully completed and close the program.

TLDR

Below you can see the full example of all the things we discussed in this blogpost combined in a fully working 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 also did 2 small refactors where I moved the server creation and gracefull shutdown into their own methods. For the readers with an eagle eye you might also noticed I have made the channels in the function read and write only in the scope of the function which gives you a few more compile time advantages to prevent you from using the channels in a wrong manner. Last but not least you can also download the boilerplate here as a starting point for your own webserver.

Looking forward to your feedback. Please share this blog with your friends and colleagues on social media.

You have disabled cookies. To leave me a comment please allow cookies at functionality level.

More Stories

Cover Image for Go client for Elasticsearch using Docker

Go client for Elasticsearch using Docker

MF

Marco Franssen /

In this blog post I would like to cover the recently released Elasticsearch 7.0-rc1 Go client for Elasticsearch. In this blogpost I want to show you a small example with a simple Docker setup using to build a Elasticsearch cluster. In my previous blogpost I covered some Docker tips and tricks we will utilize again in this blog post. Initializing your project To start with we first have to create a project folder. In this folder we will have to initialize our Go module, add our Dockerfile and…

Cover Image for Docker tips and tricks for your Go projects

Docker tips and tricks for your Go projects

MF

Marco Franssen /

In this blogpost I would like to show you some basic Docker setup I have been using so far in my Go projects. We will be looking at multi-stage Docker builds and how to utilize docker-compose. In a typical project setup in Go you would most probably start with a file main.go. In addition to that I usually add a Dockerfile for building a Docker image and a docker-compose file to easily spin up my dependencies like databases and queues. To start we create a new folder to work in and initiales th…

Cover Image for Go interfaces and type assertions

Go interfaces and type assertions

MF

Marco Franssen /

In this blog I would like to zoom in on Interfaces and type assertions in Go. Compared to language like c# and Java implementing interfaces works slightly different. In the remainder of this blog I want to give you a bit of theory and practical usecases. In case this is your first time working with Go you might want to check out this blog which shows you how to setup your development environment including a small hello world. The empty interface (interface{}) is an interface which defines zero…

Cover Image for Test and benchmark your code in go

Test and benchmark your code in go

MF

Marco Franssen /

When I started writing my first programs in Go, I noticed the tooling ships out of the box with test and benchmark features. You simply follow some naming conventions when it comes to file names. You import a reference to the "testing" package which is kind of part of the language. Aaaand… Ready set, and of you Go with writing some tests and benchmarks in Go. In my earlier blog post I briefly touched writing a test in Go already. I recommend reading this blogpost whenever you are a real newby wi…