Go webserver with gracefull 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.

main.goplayground
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 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.

1
2
3
4
5
6
7
8
9
10
11
$ 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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.

1
2
3
4
$ ./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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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.

main.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
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.

Share