Building Web Servers in Go

When I first set out to build Gophish 5 years ago, I used it as an opportunity to learn Golang.

One of the things that makes Go so powerful is the batteries-included standard library, which makes it a breeze to write servers. In this post, we’ll walk through how to build web servers, starting from a basic Hello World and ending with a web server that:

If you just want the final code, you can find it in my http-boilerplate repo on Github.

Hello World!

It’s incredibly quick to create an HTTP server in Go. Here’s a simple example that implements a single handler that returns “Hello World!”:

package main

import (
	"fmt"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		fmt.Fprintf(w, "Hello World!")
	})
	http.ListenAndServe(":80", nil)
}

Running this and opening your browser to http://localhost, you’ll see the “Hello World!” response.

We’ll build on this skeleton throughout the post, but before getting fancy, let’s talk a little bit about how this works.

How net/http Works

This example uses the net/http package, which is the backbone for building HTTP clients and servers in Go. To better understand what we’re building, we should very briefly cover three important concepts: http.Handler, http.ServeMux, and http.Server.

HTTP Handlers

When a request is received, a handler processes it and writes out a response. Go implements handlers as an interface with the following signature:

type Handler interface {
        ServeHTTP(ResponseWriter, *Request)
}

Our first example uses the helper function http.HandleFunc, which wraps a function that takes an http.ResponseWriter and http.Request in a ServeHTTP function so that it can be used as a handler.

Having handlers as an interface is really powerful. For example, we’ll see later on that implementing middleware is done by simply making a handler whose ServeHTTP does something, then calls another handler’s ServeHTTP method.

So handlers are responsible for processing requests, but how do we know which handler to use?

Routing Requests

To determine which handler should process a request, Go uses an HTTP multiplexer. Some libraries may call this a “muxer” or a “router”, but the idea is the same. A router determines which handler to execute based on the path received. In our example above, we’re using the basic default multiplexer.

If you need more advanced routing support, you might consider using a third-party library. The gorilla/mux or go-chi/chi libraries are good alternatives that make it easy to set up middleware, wildcard routing, and more. And, most importantly, they work with the standard HTTP handlers which keeps things simple and easy to change later on.

Be careful when using complex web frameworks. These are typically very opinionated, and make it difficult to work with standard handlers. In my experience, the standard library mixed with a lightweight router is good enough for most applications.

Processing Requests

Finally, we need something that can listen for incoming connections, sending each request to the router so that it can be processed by the right handler. This is the http.Server.

As we’ll see later, the server is responsible for all the connection handling. This includes things like handling TLS, if configured. In our example, the call to http.ListenAndServe uses the default HTTP server.

With this background out of the way, let’s dive into some more complex examples.

Adding Let’s Encrypt

Our original example serves requests over HTTP. It’s always recommended to use HTTPS, where possible, and fortunately Go makes that easy.

If you already have a private key and certificate, you can change the server to use ListenAndServeTLS and providing the correct filepaths:

http.ListenAndServeTLS(":443", "cert.pem", "key.pem", nil)

But we can do better.

Let’s Encrypt is a free certificate authority that lets you renew certificates automatically. We can use the autocert package to configure our server with Let’s Encrypt support.

The easiest way to get this set up is to use the autocert.NewListener helper with the http.Serve method. This gathers and renews the TLS certificates via Let’s Encrypt, while letting the underlying HTTP server handle the requests:

http.Serve(autocert.NewListener("example.com"), nil)

Opening your browser to https://example.com will show the “Hello World!” response served over HTTPS.

If you need more customization, you can create an instance of autocert.Manager. Then, you can create your own instance of http.Server (remember: we’ve been using the default one so far) and add the manager as the server’s TLSConfig:

m := &autocert.Manager{
Cache:      autocert.DirCache("golang-autocert"),
Prompt:     autocert.AcceptTOS,
HostPolicy: autocert.HostWhitelist("example.org", "www.example.org"),
}
server := &http.Server{
    Addr:      ":443",
    TLSConfig: m.TLSConfig(),
}
server.ListenAndServeTLS("", "")

Easy as that, we have full HTTPS support with free automatic certificate renewal. Now let’s add some logic to our routes.

Adding ✨ Fancy ✨ Routes

The default router included with the standard library is good, but simple. It’s common to want more complex logic, such as setting up subrouters, creating wildcard routes, and adding parameters or pattern matching into routes.

This is where libraries like gorilla/mux and go-chi/chi can be useful. Here’s an example of showing how to set up some very basic API routes using the chi library:

First, assume we have a file called api/v1/api.go that contains the routes for our API:

// HelloResponse is the JSON representation for a customized message
type HelloResponse struct {
	Message string `json:"message"`
}

// HelloName returns a personalized JSON message
func HelloName(w http.ResponseWriter, r *http.Request) {
	name := chi.URLParam(r, "name")
	response := HelloResponse{
		Message: fmt.Sprintf("Hello %s!", name),
	}
	jsonResponse(w, response, http.StatusOK)
}

// NewRouter returns an HTTP handler that implements the routes for the API
func NewRouter() http.Handler {
	r := chi.NewRouter()
	r.Get("/{name}", HelloName)
	return r
}

We can then mount this to our main router under the api/v1/ prefix back in our main application:

// NewRouter returns a new HTTP handler that implements the main server routes
func NewRouter() http.Handler {
	router := chi.NewRouter()
    router.Mount("/api/v1/", v1.NewRouter())
    return router
}
http.Serve(autocert.NewListener("example.com"), NewRouter())

Having the ability to organize routes or use advanced routing makes it easier to structure and maintain larger applications.

Implementing Middleware

Middleware is just wrapping an HTTP handler in another handler. This lets us implement authentication, logging, compression, and more.

The pattern for middleware is straightfoward due to the simplicity of the http.Handler interface. We can just write a function that takes a handler and wraps it in another handler. Here’s an example, showing how an authentication handler might be implemented:

func RequireAuthentication(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        if !isAuthenticated(r) {
            http.Redirect(w, r, "/login", http.StatusTemporaryRedirect)
            return
        }
        // Assuming authentication passed, run the original handler
        next.ServeHTTP(w, r)
    })
}

Some third-party routers like chi provide useful middleware out-of-the-box.

Serving Static Files

Golang has support in the standard library for serving static files, such as images, Javascript, and stylesheets. This is done by using a function, http.FileServer, which returns a handler that serves files from a directory.

Here’s a simple example using our router from the previous example:

func NewRouter() http.Handler {
    router := chi.NewRouter()
    r.Get("/{name}", HelloName)

	// Set up static file serving
	staticPath, _ := filepath.Abs("../../static/")
	fs := http.FileServer(http.Dir(staticPath))
    router.Handle("/*", fs)
    
    return r

Be careful! The default http.Dir filesystem used by Go lists the contents of directories if no index.html exists. This could expose sensitive information. I have a package called unindexed which can be used as a drop-in replacement to prevent this.

Graceful Shutdown

Go version 1.8 introduced the ability to gracefully shutdown an HTTP server by calling the Shutdown() method. We can use this by starting our server in a goroutine and listening on a channel for a signal interrupt (e.g. pressing CTRL+C). Once this is received, we can give the server a few seconds to gracefully shutdown.

handler := server.NewRouter()
srv := &http.Server{
    Handler: handler,
}

go func() {
		srv.Serve(autocert.NewListener(domains...))
}()

// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c

// Attempt a graceful shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
srv.Shutdown(ctx)

Wrapping Up

The Go standard library is incredibly powerful. This post shows how we can take advantage of the flexible interfaces and built-in functionality to quickly create robust HTTP servers.

While this is a great start, I recommend checking out Cloudflare’s post on other precautions to take before exposing a Go HTTP server to the Internet. In the next part to this series, we’ll walk through how to add context (such as a database connection) and configuration to our servers.

If you’re interested in seeing an example of these techniques that you can use for your own application, check out the http-boilerplate project on Github.

Finally, if you want to see these techniques (and more!) applied in a full product, be sure to check out Gophish on Github.