Serving Static Sites with Go

24th August 2013

I've recently moved the site you're reading right now from a Sinatra application to an (almost) static one served by Go. While it's fresh in my head, here's an explanation of principles behind creating and serving static sites with Go.

Let's begin with a simple but real-world example: serving vanilla HTML and CSS files from a particular location.

Start by creating a directory to hold the project:

$ mkdir static-site
$ cd static-site

Along with an app.go file to hold our code, and some sample HTML and CSS files in a static directory.

First we use the FileServer function to create a handler which responds to all HTTP requests with the contents of a given FileSystem. For our FileSystem we're using the static directory relative to our application, but you could use any other directory on your machine (or indeed any object that implements the FileSystem interface). Next we use the Handle function to register our FileServer as the handler for all requests, and launch the server listening on port 3000.

It's worth pointing out that in Go the pattern "/" matches all request paths, rather than just the empty path.

Notice that because our static directory is set as the root of the FileSystem, we need to strip off the /static/ prefix from the request path before searching the FileSystem for the given file. We do this using the StripPrefix function.

If you've used templating in other web frameworks or languages before, this should hopefully feel familiar.

Go templates – in the way we're using them here – are essentially just named text blocks surrounded by {{define}} and {{end}} tags. Templates can be embedded into each other, as we do above where the layout template embeds both the title and body templates.

First we've added the html/template and path packages to the import statement.

We've then specified that all the requests not picked up by the static file server should be handled with a new serveTemplate function (if you were wondering, Go matches patterns based on length, with longer patterns take precedence over shorter ones).

In the serveTemplate function, we build paths to the layout file and the template file corresponding with the request. Rather than manual concatenation we use Join, which has the advantage of cleaning the path to help prevent directory traversal attacks.

We then use the ParseFiles function to bundle the requested template and layout into a template set. Finally, we use the ExecuteTemplate function to render a named template in the set, in our case the layout template.

Restart the application:

$ go run app.go
Listening...

And open localhost:3000/example.html in your browser. If you look at the source you should find the markup from both templates merged together. You might also notice that the Content-Type and Content-Length headers have automatically been set for us.

Lastly, let's make the code a bit more robust. We should:

Send a 404 response if the requested template doesn't exist.

Send a 404 response if the requested template path is a directory.

Send a 500 response if the template.ParseFiles or template.ExecuteTemplate functions throw an error, and log the detailed error message.