Go templates

Posted on Aug 12, 2020

Go comes with a built-in html/template package, that supports variables, loops, functions, and more. This allows us to simplify our handlers by delegating the display logic to a template.

Here we’ve modified our Hello World example to have only one handler for both hellos and goodbyes, by introducing the greeting URL parameter. The template then uses an {{ if eq .Greeting }} conditional to determine the type of greeting.

As a finishing touch, we include a FuncMap defining a Capitalize() function, to make sure our name is properly capitalized.

package main

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

    "github.com/go-chi/chi"
)

var funcMap = template.FuncMap{
    "Capitalize": strings.Title,
}

var tpl = template.Must(template.New("greeting").Funcs(funcMap).Parse(`
    {{ if eq .Greeting "hello" }}
        <p>Hello <strong>{{ .Name | Capitalize }}</strong>!<p/>
    {{ else }}
        <p>Goodbye <strong>{{ .Name | Capitalize }}</strong>!<p/>
    {{ end }}
`))

type tplData struct {
    Greeting, Name string
}

func NewGreetingsRouter() *chi.Mux {
    router := chi.NewRouter()

    router.Get("/{greeting}/{name}", func(w http.ResponseWriter, r *http.Request) {
        data := tplData{
            Greeting: chi.URLParam(r, "greeting"),
            Name:     chi.URLParam(r, "name"),
        }

        tpl.Execute(w, data)
    })

    return router
}

func main() {
    router := chi.NewRouter()
    router.Mount("/greeting", NewGreetingsRouter())

    http.ListenAndServe("localhost:3000", router)
}

In practice, however, you probably don’t want to define your templates in string variables like this. Instead, you would create separate HTML files and compose them into layouts to avoid duplication and ensure a consistent style.

First, we take our template and put it in a file instead. We wrap it with a {{ define "greeting" }}, so we can reference it later from the base template.

// templates/greeting.gohtml
{{ define "greeting" }}
    {{ if eq .Greeting "hello" }}
        <p>Hello <strong>{{ .Name | Capitalize }}</strong>!</p>
    {{ else }}
        <p>Goodbye <strong>{{ .Name | Capitalize }}</strong>!</p>
    {{ end }}
{{ end }}

Then we create a base template called "index" that references the greeting template and wraps it in some proper HTML boilerplate markup.

// templates/index.gohtml
{{ define "index"}}
<html>
    <head>
        <title>{{ .Greeting }}</title>
    </head>
    <body>
        {{ template "greeting" .}}
    </body>
</html>
{{ end }}

Our code stays mostly the same, except that Parse() became ParseFiles() instead, taking the template files we just created and Execute() became ExecuteTemplate(), taking a reference to the "index" template.

package main

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

    "github.com/go-chi/chi"
)

var funcMap = template.FuncMap{
    "Capitalize": strings.Title,
}

var tpl = template.Must(template.New("page").Funcs(funcMap).ParseFiles(
    "templates/index.gohtml", "templates/greeting.gohtml"))

type tplData struct {
    Greeting, Name string
}

func NewGreetingsRouter() *chi.Mux {
    router := chi.NewRouter()

    router.Get("/{greeting}/{name}", func(w http.ResponseWriter, r *http.Request) {
        data := tplData{
            Greeting: chi.URLParam(r, "greeting"),
            Name:     chi.URLParam(r, "name"),
        }

        tpl.ExecuteTemplate(w, "index", data)
    })

    return router
}

func main() {
    router := chi.NewRouter()
    router.Mount("/greeting", NewGreetingsRouter())

    http.ListenAndServe("localhost:3000", router)
}
comments powered by Disqus