Last time we wrote a simple server that says hello. That's nice, but very limited. Let's extend it to serve multiple pages and also add template caching for better performance.

Step One: Template Caching

The previous server loads and parses templates for every single request, which is very inefficient. Templates should be parsed once and cached in memory for future use.

Let's make this happen. You will need the previous server's code (which you can get here). Begin by adding a global templates variable just under the import section:

/** All the templates that we'll be using.
 */
var templates *template.Template

Global variables can be accessed by all functions (within the same source file, in this case), so it can be used anywhere.

Next, the templates need to be loaded. Insert the following at the top of main():

// Load the templates
templates = template.Must(template.ParseGlob("templates/*"))

This loads all templates and stores them in the templates variable.

We'll need a function to generate a page from a template, so add the code below:

/** Renders a template
 *
 * @param w the HTTP response writer
 * @param templateName the template's name
 * @param data source data for the template
 */
func renderTemplate(w http.ResponseWriter, templateName string, data interface{}) {
	err := templates.ExecuteTemplate(w, templateName+".html", data)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
	}
}

The hello() function can now be reduced to:

func hello(w http.ResponseWriter, r *http.Request) {
	renderTemplate(w, "helloworld", nil)
}

With that, template caching is done! Test it by running the updated server (Ctrl+shift+B or Packages => Script => Run Script in the Atom editor), and visiting http://localhost:8080/.

If all changes were made correctly then you'll see the hello world page.

IMPORTANT: A recent Atom.io's script package update (3.8.1) incorrectly sets the Current Working Directory (CWD) so it can't find the templates directory. If you experience this failure then set the CWD explicitly via Packages => Script => Configure Script. You must us an absolute path (e.g., "C:/Users/Hans/GoWork/src/scratchpad/helloserver2"). Hopefully they'll fix this soon.

Step Two: Adding More Pages

Start by creating a new page in the templates directory called goodbye.html:

<!DOCTYPE html>
<html>
  <head>
    <title>Goodbye!</title>
  </head>
  <body>
    <h1>Goodbye!</h1>
    <p>Farewell.</p>
  </body>
</html>

Now the code must be updated to serve this page when people visit http://localhost:8080/goodbye. I'll show you two ways to achieve this: a simple but tedious one, and one that makes adding pages easy.

The Simple Approach

Look at the server code you just wrote. What's the most obvious method you can think of to add another page? You most likely thought of adding a goodbye() HTTP handler function, such as:

func goodbye(w http.ResponseWriter, r *http.Request) {
	renderTemplate(w, "goodbye", nil)
}

The handler above must be added to the list via the following change in main():

http.HandleFunc("/goodbye/", goodbye)

The new page is now active. Test it by running the new server, and visiting http://localhost:8080/goodbye.

IMPORTANT: Remember to shut down the old server, or the new one won't start.

A Smarter Way

While the changes above work, it gets tedious fast. Imagine having to add 100 pages. You'd have to write 100 handler functions and 100 http.HandleFunc() calls. More annoyingly, those functions are almost identical; only the template changes. Really tedious and ugly.

We want a single handler that will choose the correct template for each URL. This can be achieved in multiple ways; my solution uses Julien Schmidt's HttpRouter package. This HTTP router offers two advantages over the standard Go router:

  • It's much faster
  • It can extract parameters from the URL and pass them to the handler functions

Parameter passing is what we're most interested in. We'll program HttpRouter to extract the page's name from the URL, and that name will match a template (for valid URLs).

First, delete renderTemplate(), hello(), and goodbye(). We won't be needing them any more.

Next, import the HttpRouter package by adding the following to the import() section:

"github.com/julienschmidt/httprouter"

Replace main() with the following:

func main() {
	// Load the templates
	templates = template.Must(template.ParseGlob("templates/*"))

	// Set up the HTTP routing
	router := httprouter.New()
	router.GET("/", pageHandler)
	router.GET("/:page", pageHandler)

	// Run the server
	err := http.ListenAndServe(":8080", router)
	if err != nil {
		log.Printf("HTTP stopped with message: %v", err)
	}
}

Notice the new routing code that uses HttpRouter:

// Set up the HTTP routing
router := httprouter.New()
router.GET("/", pageHandler)
router.GET("/:page", pageHandler)

Of particular interest is "/:page" in the last line. It tells HttpRouter to turn the next part of the URL into a parameter called "page." This is passed to pageHandler(), which is the following:

/** Handler for HTTP page requests.
 */
func pageHandler(w http.ResponseWriter, r *http.Request, params httprouter.Params) {
	// Get the page
	const indexPage = "helloworld"
	page := params.ByName("page")
	if len(page) == 0 {
		// Use the index page
		page = indexPage
	}
	tplName := page + ".html"

	// Check that the template exists
	if templates.Lookup(tplName) == nil {
		http.NotFound(w, r)
		return
	}

	// Generate the page
	err := templates.ExecuteTemplate(w, tplName, nil)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
}

This function can be broken into three stages. First, the page's name is extracted from params:

// Get the page
const indexPage = "helloworld"
page := params.ByName("page")
if len(page) == 0 {
	// Use the index page
	page = indexPage
}
tplName := page + ".html"

Notice how it extracts the "page" parameter, and generates the template's filename. An index page is provided for when no page is set (when the root/index page is visited).

The next section checks if a template exists. If not, the standard HTTP "404 Not Found" error is returned:

// Check that the template exists
if templates.Lookup(tplName) == nil {
	http.NotFound(w, r)
	return
}

Finally, if the page exists then it's sent to the browser:

// Generate the page
err := templates.ExecuteTemplate(w, tplName, nil)
if err != nil {
	http.Error(w, err.Error(), http.StatusInternalServerError)
	return
}

Getting HttpRouter

Nearly there! Our updated server won't compile until the HttpRouter package installed on our machine. Fortunately Go makes this easy. Save the file (if you haven't done so already). Next, open a console/terminal window (a.k.a., "Command Prompt" in Windows; click on the start menu and type "cmd"). Navigate to the directory containing your server code (e.g., "cd GoWork/src/scratchpad/helloserver"). Now execute "go get." HttpRouter is now installed.

Testing

Start the server as before, and visit http://localhost:8080/. You should see the hello world page as usual. Now visit http://localhost:8080/goodbye, and you should see the goodbye page. Finally, try visiting a page that doesn't exist (e.g., http://localhost:8080/error). You should see a 404 error.

The server's goodbye page

Adding Even More Pages

More pages can be added simply by adding new *.html files to the templates directory and restarting the server. There's no more need to write handlers.

If you're worried about the security of mapping a URL segment to a template file, HttpRouter does a good job of filtering out the usual attempts to access files outside the templates directory.

Conclusion

Our little file server can now serve multiple pages, and adding those pages is very easy. It's still very limited though. It can't serve additional content such as images or CSS files. We've made enough progress for today, so that's a task for next time.