Go Templates: text/template and html/template

Go's standard library includes two template packages that share identical syntax but serve different purposes. The `text/template` package generates plain text output for configuration files, emails,...

Key Insights

  • Go provides two template packages with identical syntax but different escaping: text/template for plain text and html/template for web content with automatic XSS protection
  • Template composition through define, template, and block enables powerful layout inheritance patterns that eliminate duplication across pages
  • Parse templates once at application startup and cache them in memory—reparsing on every request kills performance and creates unnecessary overhead

Introduction to Go’s Template Packages

Go’s standard library includes two template packages that share identical syntax but serve different purposes. The text/template package generates plain text output for configuration files, emails, or code generation. The html/template package produces HTML with context-aware escaping to prevent cross-site scripting (XSS) attacks.

Both packages use the same template language with actions delimited by {{ and }}. The critical difference is escaping: html/template analyzes your template structure and automatically escapes data based on context (HTML body, attribute, JavaScript, CSS, or URL), while text/template outputs everything verbatim.

Choose html/template for any web-facing content. Choose text/template for everything else. Never use text/template to generate HTML that includes user input—you’ll create security vulnerabilities.

text/template Fundamentals

Templates operate on data structures passed at execution time. The current data object is referenced as . (dot). Access struct fields with .FieldName and map values with .key.

package main

import (
    "os"
    "text/template"
)

type User struct {
    Name  string
    Email string
    Admin bool
}

func main() {
    tmpl := `Hello {{.Name}}!
Your email is: {{.Email}}
{{if .Admin}}You have admin privileges.{{end}}`

    t := template.Must(template.New("user").Parse(tmpl))
    
    user := User{Name: "Alice", Email: "alice@example.com", Admin: true}
    t.Execute(os.Stdout, user)
}

Range loops iterate over slices, arrays, and maps. Inside a range block, . becomes the current element:

tmpl := `Users:
{{range .}}
- {{.Name}} ({{.Email}})
{{end}}`

users := []User{
    {Name: "Alice", Email: "alice@example.com"},
    {Name: "Bob", Email: "bob@example.com"},
}

t := template.Must(template.New("list").Parse(tmpl))
t.Execute(os.Stdout, users)

Pipelines pass values through multiple operations. The | operator chains functions together:

tmpl := `{{.Name | printf "User: %s" | println}}`

Variables store intermediate values within templates:

tmpl := `{{$name := .Name}}
Hello {{$name}}!
Goodbye {{$name}}!`

Conditionals support if, else if, and else. Empty values (0, false, nil, empty strings/slices/maps) are considered false:

tmpl := `{{if .Admin}}
Admin user
{{else if .Moderator}}
Moderator user
{{else}}
Regular user
{{end}}`

html/template and Auto-Escaping

The html/template package prevents XSS attacks by escaping data based on context. It understands HTML structure and applies appropriate escaping for element content, attributes, JavaScript, CSS, and URLs.

package main

import (
    "html/template"
    "os"
)

func main() {
    tmpl := `<div>{{.Content}}</div>
<a href="{{.URL}}">Link</a>
<script>var name = "{{.Name}}";</script>`

    t := template.Must(template.New("page").Parse(tmpl))
    
    data := map[string]string{
        "Content": "<script>alert('XSS')</script>",
        "URL":     "javascript:alert('XSS')",
        "Name":    `"; alert('XSS'); "`,
    }
    
    t.Execute(os.Stdout, data)
}

Output shows context-aware escaping:

<div>&lt;script&gt;alert(&#39;XSS&#39;)&lt;/script&gt;</div>
<a href="#ZgotmplZ">Link</a>
<script>var name = "\u0022; alert('XSS'); \u0022";</script>

Notice how dangerous content is neutralized: HTML tags are escaped, the JavaScript URL is replaced with #ZgotmplZ (indicating rejected content), and the JavaScript string is properly escaped.

Sometimes you have trusted HTML that shouldn’t be escaped. Use template.HTML to mark content as safe:

data := map[string]interface{}{
    "Content": template.HTML("<strong>Bold text</strong>"),
}

Only use template.HTML for content you control. Never wrap user input directly—validate and sanitize first.

Template Functions and Custom Functions

Go templates include built-in functions like len, index, print, printf, and comparison operators (eq, ne, lt, le, gt, ge).

tmpl := `{{if gt (len .Items) 0}}
Found {{len .Items}} items
{{range $i, $item := .Items}}
Item {{$i}}: {{$item}}
{{end}}
{{end}}`

Add custom functions with FuncMap:

package main

import (
    "html/template"
    "os"
    "strings"
    "time"
)

func main() {
    funcMap := template.FuncMap{
        "upper": strings.ToUpper,
        "formatDate": func(t time.Time) string {
            return t.Format("2006-01-02")
        },
        "add": func(a, b int) int {
            return a + b
        },
    }
    
    tmpl := `Name: {{.Name | upper}}
Date: {{.Date | formatDate}}
Total: {{add .Price .Tax}}`
    
    t := template.Must(template.New("page").Funcs(funcMap).Parse(tmpl))
    
    data := map[string]interface{}{
        "Name":  "alice",
        "Date":  time.Now(),
        "Price": 100,
        "Tax":   15,
    }
    
    t.Execute(os.Stdout, data)
}

Functions must be registered before parsing. Chain functions in pipelines for complex transformations:

{{.Email | lower | printf "Contact: %s"}}

Template Composition

Real applications need layouts, headers, footers, and reusable components. Go templates support composition through named templates.

Define templates with {{define "name"}}...{{end}}:

const templates = `
{{define "base"}}
<!DOCTYPE html>
<html>
<head><title>{{template "title" .}}</title></head>
<body>
    {{template "header" .}}
    <main>{{template "content" .}}</main>
    {{template "footer" .}}
</body>
</html>
{{end}}

{{define "header"}}
<header><h1>My Site</h1></header>
{{end}}

{{define "footer"}}
<footer>&copy; 2024</footer>
{{end}}
`

Page templates extend the base:

const homePage = `
{{define "title"}}Home{{end}}

{{define "content"}}
<h2>Welcome {{.Username}}</h2>
<p>This is the home page.</p>
{{end}}
`

t := template.Must(template.New("").Parse(templates))
t = template.Must(t.Parse(homePage))

data := map[string]string{"Username": "Alice"}
t.ExecuteTemplate(os.Stdout, "base", data)

The block action provides default content that can be overridden:

{{define "base"}}
<html>
<body>
    {{block "sidebar" .}}
    <aside>Default sidebar</aside>
    {{end}}
    
    {{block "content" .}}
    <p>Default content</p>
    {{end}}
</body>
</html>
{{end}}

If a page doesn’t define “sidebar”, the default appears. If it does, the default is replaced.

Practical Patterns and Best Practices

Parse templates once at startup, not on every request. Reparsing is expensive and wastes CPU:

type Templates struct {
    templates *template.Template
}

func NewTemplates() (*Templates, error) {
    t := template.New("").Funcs(funcMap)
    
    t, err := t.ParseGlob("templates/*.html")
    if err != nil {
        return nil, err
    }
    
    return &Templates{templates: t}, nil
}

func (t *Templates) Render(w io.Writer, name string, data interface{}) error {
    return t.templates.ExecuteTemplate(w, name, data)
}

Use template.Must to panic on parse errors during initialization. This fails fast rather than serving broken pages:

var templates = template.Must(template.ParseGlob("templates/*.html"))

Handle execution errors properly. Template execution can fail if data doesn’t match expectations:

func (t *Templates) Render(w http.ResponseWriter, name string, data interface{}) {
    err := t.templates.ExecuteTemplate(w, name, data)
    if err != nil {
        log.Printf("Template error: %v", err)
        http.Error(w, "Internal Server Error", 500)
    }
}

For development, reload templates on each request:

func (t *Templates) Render(w http.ResponseWriter, name string, data interface{}) {
    if devMode {
        t.templates = template.Must(template.ParseGlob("templates/*.html"))
    }
    t.templates.ExecuteTemplate(w, name, data)
}

Real-World Application

Here’s a complete web application with layouts and multiple pages:

package main

import (
    "html/template"
    "log"
    "net/http"
    "time"
)

var templates *template.Template

type PageData struct {
    Title    string
    Username string
    Posts    []Post
}

type Post struct {
    Title     string
    Body      string
    CreatedAt time.Time
}

func init() {
    funcMap := template.FuncMap{
        "formatDate": func(t time.Time) string {
            return t.Format("Jan 2, 2006")
        },
    }
    
    templates = template.Must(
        template.New("").Funcs(funcMap).ParseGlob("templates/*.html"),
    )
}

func main() {
    http.HandleFunc("/", homeHandler)
    http.HandleFunc("/posts", postsHandler)
    
    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
    data := PageData{
        Title:    "Home",
        Username: "Alice",
    }
    
    if err := templates.ExecuteTemplate(w, "home", data); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

func postsHandler(w http.ResponseWriter, r *http.Request) {
    data := PageData{
        Title:    "Posts",
        Username: "Alice",
        Posts: []Post{
            {Title: "First Post", Body: "Content here", CreatedAt: time.Now()},
            {Title: "Second Post", Body: "More content", CreatedAt: time.Now()},
        },
    }
    
    if err := templates.ExecuteTemplate(w, "posts", data); err != nil {
        http.Error(w, err.Error(), 500)
    }
}

Base template (templates/base.html):

{{define "base"}}
<!DOCTYPE html>
<html>
<head>
    <title>{{.Title}} - My Site</title>
</head>
<body>
    {{template "header" .}}
    <main>{{template "content" .}}</main>
    {{template "footer" .}}
</body>
</html>
{{end}}

{{define "header"}}
<header>
    <h1>My Site</h1>
    <p>Welcome, {{.Username}}</p>
</header>
{{end}}

{{define "footer"}}
<footer>&copy; 2024</footer>
{{end}}

Home page (templates/home.html):

{{template "base" .}}
{{define "content"}}
<h2>Welcome Home</h2>
<p>This is the home page.</p>
{{end}}

Posts page (templates/posts.html):

{{template "base" .}}
{{define "content"}}
<h2>Blog Posts</h2>
{{range .Posts}}
<article>
    <h3>{{.Title}}</h3>
    <time>{{.CreatedAt | formatDate}}</time>
    <p>{{.Body}}</p>
</article>
{{end}}
{{end}}

This architecture scales well. Add new pages by creating templates that extend “base”. Share components across pages by defining reusable template blocks. Keep your template logic simple and move complex operations to Go code—templates should focus on presentation, not business logic.

Liked this? There's more.

Every week: one practical technique, explained simply, with code you can use immediately.