Serving Static Sites with Go
I've recently moved the site you're reading right now from a Sinatra/Ruby application to an (almost) static site served by Go. So 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 on disk.
Start by creating a directory to hold the project:
And then add a main.go
file to hold our code, and some simple HTML and CSS files in a
static
directory.
Once those files are created, the code we need to get up and running is wonderfully compact:
Let's step through this.
First we use the http.FileServer()
function to
create a handler which responds to all HTTP requests with the contents of a given file system. For our file
system 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 http.FileSystem
interface). Next we use the http.Handle()
function to register the file server 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.
Go ahead and run the application:
And open http://localhost:3000/example.html
in your
browser. You should see the HTML page we made with a big red heading.
Almost-Static Sites
If you're creating a lot of static HTML files by hand, it can be tedious to keep repeating boilerplate content. Let's
explore using Go's html/template
package to put shared
markup in a layout file.
At the moment all requests are being handled by our file server. Let's make a slight adjustment to our
application so the file server only handles request paths that begin with the pattern /static/
instead.
Notice that because our static
directory is set as the root of the file system, we need to strip off the
/static/
prefix from the request path before searching the file system for the given file. We do
this using the http.StripPrefix()
function.
If you restart the application, you should find the CSS file we made earlier available at http://localhost:3000/static/stylesheets/main.css
.
Now let's create a templates
directory, containing a layout.html
file with shared markup,
and an example.html
file with some page-specific content.
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 using the {{template}}
tag, like we do above where the
layout
template embeds both the title
and body
templates.
Let's update the application code to use these:
So what's changed here?
First we've added the html/template
and path
packages to the import statement.
Then we've 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 taking 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 filepath.Join()
, which has the advantage joining
paths using the correct separator for your OS.
Importantly, because the URL path is untrusted user input, we use filepath.Clean()
to sanitise the URL path before
using it.
(Note that even though filepath.Join()
automatically runs the joined path through
filepath.Clean()
, to help prevent directory traversal attacks you need to manually sanitise any untrusted
inputs before joining them.)
We then use the template.ParseFiles()
function to
bundle the requested template and layout into a template set. Finally, we use the template.ExecuteTemplate()
function to render a named template in the set, in our case the layout
template.
Restart the application:
And open http://localhost:3000/example.html
in your
browser. You should see the markup from all the templates merged together like so:
If you use web developer tools to inspect the HTTP response, you'll also see that Go automatically sets the correct
Content-Type
and Content-Length
headers 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 thetemplate.ParseFiles()
ortemplate.ExecuteTemplate()
functions throw an error, and log the detailed error message.