Getting started with Go?

My book guides you through the start-to-finish build of a real world web application in Go — covering topics like how to structure your code, manage dependencies, create dynamic database-driven pages, and how to authenticate and authorize users securely.

Take a look!

Serving Static Sites with Go

Last updated:

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:

$ mkdir static-site
$ cd static-site

And then add a main.go file to hold our code, and some simple HTML and CSS files in a static directory.

$ touch main.go
$ mkdir -p static/stylesheets
$ touch static/example.html static/stylesheets/main.css
File: static/example.html
<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>A static page</title>
	<link rel="stylesheet" href="/stylesheets/main.css">
</head>
<body>
	<h1>Hello from a static page</h1>
</body>
</html>
File: static/stylesheets/main.css
body {color: #c0392b}

Once those files are created, the code we need to get up and running is wonderfully compact:

File: main.go
package main

import (
	"log"
	"net/http"
)

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/", fs)

	log.Print("Listening on :3000...")
	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

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:

$ go run main.go
Listening on :3000...

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.

File: main.go
...

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	log.Print("Listening on :3000...")
	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

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.

$ mkdir templates
$ touch templates/layout.html templates/example.html
File: templates/layout.html
{{define "layout"}}
<!doctype html>
<html>
<head>
	<meta charset="utf-8">
	<title>{{template "title"}}</title>
	<link rel="stylesheet" href="/static/stylesheets/main.css">
</head>
<body>
	{{template "body"}}
	<footer>Made with Go</footer>
</body>
</html>
{{end}}
File: templates/example.html
{{define "title"}}A templated page{{end}}

{{define "body"}}
<h1>Hello from a templated page</h1>
{{end}}

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:

File: main.go
package main

import (
	"html/template"
	"log"
	"net/http"
	"path/filepath"
)

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))

	http.HandleFunc("/", serveTemplate)

	log.Print("Listening on :3000...")
	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func serveTemplate(w http.ResponseWriter, r *http.Request) {
	lp := filepath.Join("templates", "layout.html")
	fp := filepath.Join("templates", filepath.Clean(r.URL.Path))

	tmpl, _ := template.ParseFiles(lp, fp)
	tmpl.ExecuteTemplate(w, "layout", nil)
}

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:

$ go run main.go
Listening on :3000...

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 the template.ParseFiles() or template.ExecuteTemplate() functions throw an error, and log the detailed error message.
File: main.go
package main

import (
	"html/template"
	"log"
	"net/http"
	"os"
	"path/filepath"
)

func main() {
	fs := http.FileServer(http.Dir("./static"))
	http.Handle("/static/", http.StripPrefix("/static/", fs))
	http.HandleFunc("/", serveTemplate)

	log.Print("Listening on :3000...")
	err := http.ListenAndServe(":3000", nil)
	if err != nil {
		log.Fatal(err)
	}
}

func serveTemplate(w http.ResponseWriter, r *http.Request) {
	lp := filepath.Join("templates", "layout.html")
	fp := filepath.Join("templates", filepath.Clean(r.URL.Path))

	// Return a 404 if the template doesn't exist
	info, err := os.Stat(fp)
	if err != nil {
		if os.IsNotExist(err) {
			http.NotFound(w, r)
			return
		}
	}

	// Return a 404 if the request is for a directory
	if info.IsDir() {
		http.NotFound(w, r)
		return
	}

	tmpl, err := template.ParseFiles(lp, fp)
	if err != nil {
		// Log the detailed error
		log.Print(err.Error())
		// Return a generic "Internal Server Error" message
		http.Error(w, http.StatusText(500), 500)
		return
	}

	err = tmpl.ExecuteTemplate(w, "layout", nil)
	if err != nil {
		log.Print(err.Error())
		http.Error(w, http.StatusText(500), 500)
	}
}