bentranter.io

Creating a Static File Server with Go

Published

The simplest way to get a website up and running is to create a fully static site, that is, a site composed exclusively of unchanging content. The server delivers the same files to each user.

Go makes this easy with the net/http package, because it has a static file server.

In this post, we'll create two static file severs: one for CSS files and images, and another for raw HTML templates.

Start by changing directories to your $GOPATH:

cd ~/go/src/github.com/<your GitGub username> # Change directories to your GOPATH.
mkdir blog                                    # Make a new directory named "blog".
cd blog                                       # Change directories to the new blog directory.
touch README.md main.go                       # Create the README.md and main.go files.

In main.go, type:

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello world"))
	})
	log.Println("Server started on http://localhost:3000")
	http.ListenAndServe(":3000", nil)
}

Start your server by running go run main.go. If you visit http://localhost:3000, you should see this.

Web server open in Safari

Next, we'll add the ability to serve static files. We'll add a static file server, and register the route with our router.

package main

import (
	"log"
	"net/http"
)

func main() {
	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("hello world"))
	})

fs := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fs))
log.Println("Server started on http://localhost:3000") http.ListenAndServe(":3000", nil) }

After that, create the static directory that we're serving:

mkdir static

Then, create an index.html file there:

touch static/index.html

In that file, type:

<!DOCTYPE html>
<html>
<head>
	<title>Blog</title>
</head>
<body>
	<h1>Blog</h1>
</body>
</html>

Start your server again by running

go run main.go

and visit http://localhost:3000/static. You'll see that the index.html file is being served:

index.html open in Safari

However, this isn't ideal for two reasons:

  1. We don't want our entire web app to exist on the /static path.
  2. We have no way of passing data to our HTML page.

To change this, we can change our "/" handler to serve HTML templates.

package main

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

func main() {
	templates, err := template.ParseGlob("templates/*")
	if err != nil {
		log.Fatalf("failed to parse templates directory: %v", err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("hello world"))
path := r.URL.Path
if err := templates.ExecuteTemplate(w, path, nil); err != nil {
log.Printf("error executing template %s: %v", path, err)
}
}) fs := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", fs)) log.Println("Server started on http://localhost:3000") http.ListenAndServe(":3000", nil) }

We need to create this templates directory, and move our index.html under it:

mkdir templates                           # Create the templates directory.
mv static/index.html templates/index.html # Move the index.html file from the static directory to the templates directory.

Now we can run our server again:

go run main.go

And visit http://localhost:3000.

Blank output in Safari

But nothing shows up. If you check the output in your terminal, you'll see output that looks like,

2018/08/07 02:59:55 error executing template /index: html/template: "/index" is undefined

To fix this, we need to add a check that serve the index.html when the value of path is "/".

package main

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

func main() {
	templates, err := template.ParseGlob("templates/*")
	if err != nil {
		log.Fatalf("failed to parse templates directory: %v", err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path

if path == "/" {
path = "index"
}
if err := templates.ExecuteTemplate(w, path, nil); err != nil { log.Printf("error executing template %s: %v", path, err) } }) fs := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", fs)) log.Println("Server started on http://localhost:3000") http.ListenAndServe(":3000", nil) }

However, this code will still cause an error that looks like:

2018/08/07 03:04:42 error executing template index: html/template: "index" is undefined

At this point, the solution is pretty obvious: we need to add the .html file extension so that we can an exact match on the template:

package main

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

func main() {
	templates, err := template.ParseGlob("templates/*")
	if err != nil {
		log.Fatalf("failed to parse templates directory: %v", err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path

		if path == "/" {
			path = "index"
		}

if err := templates.ExecuteTemplate(w, path
+".html"
, nil); err != nil {
log.Printf("error executing template %s: %v", path, err) } }) fs := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", fs)) log.Println("Server started on http://localhost:3000") http.ListenAndServe(":3000", nil) }

We're almost there. There's still one issue left with our code. Let's add another HTML file in our templates folder, which will make the last issue obvious:

touch templates/about.html

In that file, we'll add some HTML:

<!DOCTYPE html>
<html>
<head>
	<title>About</title>
</head>
<body>
	<h1>About</h1>
</body>
</html>

Run our server with:

go run main.go

If you open your browser to http://localhost:3000/about, you'll still see nothing. If you check the terminal output, you'll see:

2018/08/07 03:11:23 error executing template /about: html/template: "/about.html" is undefined

The issue is the leading "/" character. The template lookup function look for a file named "/about.html" in the the templates directory, but we've only got a file named "about.html". To fix this, we need to strip that leading "/":

package main

import (
	"html/template"
	"log"
	"net/http"
	"strings"
)

func main() {
	templates, err := template.ParseGlob("templates/*")
	if err != nil {
		log.Fatalf("failed to parse templates directory: %v", err)
	}

	http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		path := r.URL.Path

		if path == "/" {
			path = "index"
		}

path = strings.TrimPrefix(path, "/")
if err := templates.ExecuteTemplate(w, path+".html", nil); err != nil { log.Printf("error executing template %s: %v", path, err) } }) fs := http.FileServer(http.Dir("static")) http.Handle("/static/", http.StripPrefix("/static/", fs)) log.Println("Server started on http://localhost:3000") http.ListenAndServe(":3000", nil) }

Now, if we run our code:

go run main.go

and open our browser to http://localhost:3000/about, we'll see:

About page open in Safari

Any HTML file we place under /posts will now be served at /posts/<filename>>. While this is very basic, it gives us a fully functioning website.

In the next post, we'll see how to use the html/template package to render HTML content dynamically.


Enjoyed this article? Found a bug? Whatever your needs, feel free to email me at ben at bentranter dot io.