12

I'm looking to see if there is a better (faster, more organised) way to split up my templates in Go. I strongly prefer to stick to html/template (or a wrapper thereof) since I trust its security model.

  • Right now I use template.ParseGlob to parse all of my template files in within init().
  • I apply template.Funcs to the resulting templates
  • I set a $title in each template (i.e. listing_payment.tmpl) and pass this to the content template.
  • I understand that html/template caches templates in memory once parsed
  • My handlers only call t.ExecuteTemplate(w, "name.tmpl", map[string]interface{}) and don't do any silly parsing on each request.
  • I compose templates from multiple pieces (and this is the bit I find clunky) as below:

    {{ $title := "Page Title" }}
    {{ template "head" $title }}
    {{ template "checkout" }}
    {{ template "top" }}
    {{ template "sidebar_details" . }}
    {{ template "sidebar_payments" }}
    {{ template "sidebar_bottom" }}
    
    <div class="bordered-content">
      ...
          {{ template "listing_content" . }}
      ...
    </div>
    
    {{ template "footer"}}
    {{ template "bottom" }}
    

My three questions are:

  1. Is this performant, or do the multiple {{ template "name" }} tags result in a potential per-request performance hit? I see a lot of write - broken pipe errors when stress testing heavier pages. This might just be due to socket timeouts (i.e. socket closing before the writer can finish) rather than some kind of per-request composition, though (correct me if otherwise!)

  2. Is there a better way to do this within the constraints of the html/template package? The first example in Django's template docs approaches what I'd like. Extend a base layout and replace the title, sidebar and content blocks as needed.

  3. Somewhat tangential: when template.ExecuteTemplate returns an error during a request, is there an idiomatic way to handle it? If I pass the writer to an error handler I end up with soup on the page (because it just continues writing), but a re-direct doesn't seem like idiomatic HTTP.

elithrar
  • 20,066
  • 10
  • 74
  • 93

1 Answers1

11

With some help on Reddit I managed to work out a fairly sensible (and performant) approach to this that allows:

  • Building layouts with content blocks
  • Creating templates that effectively "extend" these layouts
  • Filling in blocks (scripts, sidebars, etc.) with other templates

base.tmpl

<html>
<head>
    {{ template "title" .}}
</head>
<body>
    {{ template "scripts" . }}
    {{ template "sidebar" . }}
    {{ template "content" . }}
<footer>
    ...
</footer>
</body>

index.tmpl

{{ define "title"}}<title>Index Page</title>{{ end }}
// We must define every block in the base layout.
{{ define "scripts" }} {{ end }} 
{{ define "sidebar" }}
    // We have a two part sidebar that changes depending on the page
    {{ template "sidebar_index" }} 
    {{ template "sidebar_base" }}
{{ end }}
{{ define "content" }}
    {{ template "listings_table" . }}
{{ end }}

... and our Go code, which leverages the map[string]*template.Template approach outlined in this SO answer:

var templates map[string]*template.Template

var ErrTemplateDoesNotExist = errors.New("The template does not exist.")

// Load templates on program initialisation
func init() {
    if templates == nil {
        templates = make(map[string]*template.Template)
    }

    templates["index.html"] = template.Must(template.ParseFiles("index.tmpl", "sidebar_index.tmpl", "sidebar_base.tmpl", "listings_table.tmpl", "base.tmpl"))
    ...
}

// renderTemplate is a wrapper around template.ExecuteTemplate.
func renderTemplate(w http.ResponseWriter, name string, data map[string]interface{}) error {
    // Ensure the template exists in the map.
    tmpl, ok := templates[name]
    if !ok {
        return ErrTemplateDoesNotExist
    }

    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    tmpl.ExecuteTemplate(w, "base", data)

    return nil
}

From initial benchmarks (using wrk) it seems to be a fair bit more performant when it comes to heavy load, likely due to the fact that we're not passing around a whole ParseGlob worth of templates every request. It also makes authoring the templates themselves a lot simpler.

Community
  • 1
  • 1
elithrar
  • 20,066
  • 10
  • 74
  • 93
  • I'm super new to Go and trying to solve this problem. Is the `data` parameter just your hash of params that are sent to the template? – wuliwong Oct 27 '15 at 02:50
  • @wuliwong Since data is an `interface{}` it can be whatever you want it to be. I typically pass a `map[string]interface{}` where the map keys are things like "user", "itemList", etc. – elithrar Oct 27 '15 at 04:17
  • A co-worked pointed out that `.ExecuteTemplate` takes params the same way as `.Execute`. My problem was elsewhere. I was passing the parameters to the nested templates. Thanks for the response! – wuliwong Oct 27 '15 at 16:23
  • What about if you have user roles and every role have a different content menu? Creating a different templates["index.html"] for every role? ok then seems the only solution is using your approach with web components in order to load the sub-content for every role without create a new template. – chespinoza Mar 09 '16 at 14:35
  • 1
    @cespinoza Either load that when you render the template or use the new block feature in Go 1.6 - https://tip.golang.org/doc/go1.6#template – elithrar Mar 09 '16 at 14:37
  • Hey Matt @elithrar, what do you think about that? http://stackoverflow.com/questions/35907031/golang-templates-a-better-approach-for-role-based-views please could you give me a hand? – chespinoza Mar 10 '16 at 03:34