How To Create A Template Cache For Your Golang Web Application
How to create a reusable template cache for a go web application that allows the application to read templates from memory.
Note: This example covers creating an in-memory cache that is uses an
external folder to store templates. If you want to learn how to use the
go:embed
package to create a template cache, go here: Use Go Embed Package for Template Rendering.
A beginner’s guide to create a reusable template cache for your Go web application: eveything you need to get started. For example the net/http
and html/template
package are part of the standard library and allow us to create full-featured web applications with just the standard library.
Template Cache In Go Web Applications: A Beginner’s Guide
Getting started creating a web application with Go isn’t difficult. One area that was difficult for me was the idea of creating a template cache to serve Go template files.
Note: This example actually uses material from Trevor Sawler’s udemy course which can be purchased here: Building Modern Web Applications With Go
Reading templates from the filesystem is pretty straightforward. But what if you want to read all the templates onces and store them in an in-memory app? Well that is entirely possible of course. It does take a little bit of work that was confusing for me at first. The tricky part, in my opinion, was learning how to create a site-wide app configuration and passing the data around the app. For this particulary method, we use an AppConfig to store the template cache as a map. Then to pass the data to the rendering engine, we create a helper function inside of the rendering engine to get the AppConfig and have access to it.
- Create a new go application and run
go mod init basic-web-app
wherebasic-web-app
is the location of your project repo such as github or gitlab. - Create a new directory in the root of your project at
cmd/web
- Inside of
cmd/web
we want to create ourmain.go
file - At the root of your project directory, you will want to create a new folder called
pkg
- Inside of
pkg
, create 3 new directoriesconfig
,render
,handlers
. For each new folder respectively, create a new go file named the same as the folder. For example,config.go
,handlers.go
, andrender.go
- Again, at the root of the project directory, we need to also create a folder named
templates
which will store our respective templates. Inside of thetemplates
directory we need to create two templates:base.layout.tmpl
andhome.page.tmpl
These will be the go html templates to render our pages. These will need to be populated like so:
@@ base.layout.tmpl will look like this
{{ define "base" }}
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ template "title" . }}</title>
</head>
<body>
{{ template "Content" . }}
</body>
</html>
{{ end }}
@@ home.page.tmpl will look like this
{{ template "base" }}
{{ define "title" }}
My home Page
{{ end }}
{{ define "Content" }}
<p>This is some content</p>
{{ end }}
- Next, in the render package, I want to create a new function called
CreateTemplateCache
to handle creating the templates. This file will create a new map that will hold our template sets. Inside of the render package create a new function that looks something like this.
// CreateTemplateCache finds all of the templates and returns a map of pointers to the templates
func CreateTemplateCache() (map[string]*template.Template, error) {
// create a new map to hold our templates
myCache := map[string]*template.Template{}
// using go built-in filepath module, blog the pages
pages, err := filepath.Glob("./templates/*.page.tmpl")
// handle the filepath.Glob error if there is one
if err != nil {
log.Fatalln(err)
}
// next we need to loop through all the pages and create a new template set
for _, page := range pages {
// first we need to get the actual name of the page using filepath.Base
name := filepath.Base(page)
// next, we need to actually create the template set
ts, err := template.New(name).ParseFiles(page)
// again, if there are any errors creating the template set and parsing the files
if err != nil {
log.Fatalln(err)
}
// Next, the template set needs to know of any layouts we are using so it can parse correctly
matches, err := filepath.Glob("./templates/*.layout.tmpl")
// check for errors
if err != nil {
log.Fatalln(err)
}
// check if matches are greater than zero (layouts exist)
if len(matches) > 0 {
// using the template set from above, we continue to parse the layouts
ts, err = ts.ParseGlob("./templates/*.layout.tmpl")
// check for errors
if err != nil {
log.Fatalln(err)
}
// finally, once we parse all of the elements of the page we can add it to the template cache
myCache[name] = ts
}
}
return myCache, err
}
- Next, we are going to want to create a function to actually handle rendering the templates. This function will need access to our templateCache which we are going to store in our config package. We are going to reference or config from memory through a variable called
app
. To get started, create a fyuncion calledRender Template
inside of therender
package.
// RenderTemplate renders templates using html/template
func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string) error {
// holds the templates for easy access
var tc map[string]*template.Template
// app.UseCache checks to see if we are in development or production and decides how we get our templates to render
if app.UseCache {
// get the template cache from the app config
tc = app.TemplateCache
} else {
// this is just used for testing, so that we rebuild
// the cache on every request
tc, _ = CreateTemplateCache()
}
// does the template exist in the templateCache?
t, ok := tc[tmpl]
if !ok {
return errors.New("can't get template from cache")
}
// Create a new bytes butter
buf := new(bytes.Buffer)
// execute the template to the buffer
err := t.Execute(buf, nil)
if err != nil {
log.Fatal(err)
}
// write the buffer to http.ResponseWriter
_, err = buf.WriteTo(w)
if err != nil {
fmt.Println("Error writing template to browser", err)
return err
}
// return err
return nil
}
- Lets head over to our config package and set that up to get the templatecache to work. We will need to create a new type
AppConfig
to hold our templateCache in memory for us.
package config
import "html/template"
type AppConfig struct {
TemplateCache map[string]*template.Template
UseCache bool
}
- Back in
main.go
we are going to need to initialize our AppConfig and Store our templates in it
package main
import (
"basic-web-app/pkg/config"
"basic-web-app/pkg/render"
"log"
"net/http"
)
func main() {
// initalize app config
var app config.AppConfig
// render the templates once
tc, err := render.CreateTemplateCache()
// handle err
if err != nil {
log.Fatalln(err)
}
// store the templatecache inside of the AppConfig
app.TemplateCache = tc
// Turn on production mode to read templates from memory cache
app.UseCache = true
// create a new mux
srv := http.NewServeMux()
srv.HandleFunc("/", handlers.Home)
http.ListenAndServe(":8080", srv)
}
- Next, we are going to need to go back into render and create a little helper function. This function will be called
NewRenderer
and it’s only job right now is to bring the AppConfig into the render package so we have access to it.render.go
will now look like this:
package render
import (
"basic-web-app/pkg/config"
"bytes"
"errors"
"fmt"
"html/template"
"log"
"net/http"
"path/filepath"
)
var app *config.AppConfig
func NewRender(a *config.AppConfig) {
app = a
}
// RenderTemplate renders templates using html/template
func RenderTemplate(w http.ResponseWriter, r *http.Request, tmpl string) error {
// holds the templates for easy access
var tc map[string]*template.Template
// app.UseCache checks to see if we are in development or production and decides how we get our templates to render
if app.UseCache {
// get the template cache from the app config
tc = app.TemplateCache
} else {
// this is just used for testing, so that we rebuild
// the cache on every request
tc, _ = CreateTemplateCache()
}
// does the template exist in the templateCache?
t, ok := tc[tmpl]
if !ok {
return errors.New("can't get template from cache")
}
// Create a new bytes butter
buf := new(bytes.Buffer)
// execute the template to the buffer
err := t.Execute(buf, nil)
if err != nil {
log.Fatal(err)
}
// write the buffer to http.ResponseWriter
_, err = buf.WriteTo(w)
if err != nil {
fmt.Println("Error writing template to browser", err)
return err
}
// return err
return nil
}
func CreateTemplateCache() (map[string]*template.Template, error) {
// create a new map to hold our templates
myCache := map[string]*template.Template{}
// using go built-in filepath module, blog the pages
pages, err := filepath.Glob("./templates/*.page.tmpl")
// handle the filepath.Glob error if there is one
if err != nil {
log.Fatalln(err)
}
// next we need to loop through all the pages and create a new template set
for _, page := range pages {
// first we need to get the actual name of the page using filepath.Base
name := filepath.Base(page)
// next, we need to actually create the template set
ts, err := template.New(name).ParseFiles(page)
// again, if there are any errors creating the template set and parsing the files
if err != nil {
log.Fatalln(err)
}
// Next, the template set needs to know of any layouts we are using so it can parse correctly
matches, err := filepath.Glob("./templates/*.layout.tmpl")
// check for errors
if err != nil {
log.Fatalln(err)
}
// check if matches are greater than zero (layouts exist)
if len(matches) > 0 {
// using the template set from above, we continue to parse the layouts
ts, err = ts.ParseGlob("./templates/*.layout.tmpl")
// check for errors
if err != nil {
log.Fatalln(err)
}
// finally, once we parse all of the elements of the page we can add it to the template cache
myCache[name] = ts
}
}
return myCache, err
}
But we still need to call the
NewRender
function so we can give therender
package access to theAppConfig
. So back inmain.go
before we start the server we can do this:render.NewRender(&app)
. This will allow us to pass our data from theAppConfig
to the render package.All that is left is to actually handle our route. In the
handlers
package we need to create a new function calledHome
. This will handle our home page route.
package handlers
import (
"basic-web-app/pkg/render"
"net/http"
)
func Home(w http.ResponseWriter, r *http.Request) {
// since "/" is a catch all we need to handle what happens if it is not the home page
if r.URL.Path != "/" {
http.Error(w, "Not Found, Sorry", 404)
return
}
// if the path is correct, render the home page
render.RenderTemplate(w, "home.page.tmpl")
}
This concept was difficult for me to wrap my head around at first. I come from a Javascript background so I am not very used to the idea of working with pointers. I hope this simplified demo helps you get started on the right path with making your Golang web application more efficient if you are using built in templates.