Go Templates

I recently discovered that I was rendering Go templates on every HTTP request. Luckily, my templates are small and Go renders them quickly. Unluckily I didn’t notice the rendering overhead (ironically, because it was so small).

Before the optimization, my HTTP handler looked like this:

func myHandler(res http.ResponseWriter, req *http.Request) {
  tmpl, err := template.New("MyTmpl").ParseFiles("some_template.tmpl")
  if err != nil {
    http.Error(res, "couldn't parse template", http.StatusInternalServerError)
    return
  }
  tmpl.Execute(res, map[string]string{})
}

Notice that every request executes myHandler, so ParseFiles gets called on every request. I ran a simple benchmark that calls Parse instead of ParseFiles and still takes 6 milliseconds (reading files would add even more latency.)

Let’s eliminate that latency by removing the ParseFiles call from the handler and pre-render the templates instead.

 Pre-Rendering Templates

To do the pre-rendering, we need to put the ParseFiles calls somewhere else. We can pre-render the template by putting it in a global var:

var tmpl = template.Must(template.New("MyTmpl").ParseFiles("some_template.tmpl"))

We also added the template.Must call so the server will panic and crash on startup if the template couldn’t be parsed - an added feature.

 A Cleaner Way

Global variables are ok for small programs, but as your program gets bigger, they can lead to problems.

So instead of storing our pre-rendered templates in global scope, let’s create a closure and put the parsed template in it.

func myHandlerWrapper() func(http.ResponseWriter, *http.Request) {
  tmpl := template.Must(template.New("MyTmpl").ParseFiles("some_template.tmpl"))

  return func(res http.ResponseWriter, req *http.Request {
    //use tmpl in here
  }
}

Now we’re parsing the template and returning a func that has the template in its closure. We call myHandlerWrapper once to get the func to be passed to http.HandleFunc:

http.HandleFunc("/my_endpoint", myHandlerWrapper())

Since myHandlerWrapper gets called once, the template is still parsed once, and it will still panic immediately if the template is un-parseable.

 Other Options

This improvement is really all about where to store the pre-rendered template. I showed storing it in a closure, which is handy if you’re refactoring a bunch of http.HandleFunc calls. You can also store it in a http.Handler or some other custom structure you choose.

And finally, you can use these strategies to pass other data to your HTTP handlers. For example, in one of my projects I pass a go-bindata-html-template AssetFunc to my handlers using the closure strategy above, and then pre-compute the template using the AssetFunc:

func myHandlerWrapper(assetFunc template.AssetFunc) func(http.ResponseWriter, *http.Request) {
  tmpl := template.Must(template.New("MyTmpl").Parse(assetFunc("some_template.tmpl"))
  return func(res http.ResponseWriter, req *http.Request) {
    //use tmpl here
  }
}
 
8
Kudos
 
8
Kudos

Now read this

A Critique of Google Container Builder

At Google Cloud Next, Google announced that Google Container Builder is now generally available in beta. Very simply put, container builder is a CI service that runs pipelines of build steps. Each step is run inside a Docker container. I... Continue →