Skip to content

template

Official documentation: template package - text/template - Go Packages

We often use the fmt.Sprintf function for string formatting, but it's only suitable for handling small strings and requires format verbs to specify types. It doesn't support named parameters or handle complex situations. This is the problem that template engines solve. For example, static HTML pages served by a backend need to use a template engine. There are many excellent third-party template engine libraries in the community, such as pongo2, sprig, and jet. However, this article focuses on Go's built-in template engine library text/template. In actual development, html/template is generally used, which is based on the former and has made many security treatments for HTML. The former is generally sufficient for most cases, but if you're dealing with HTML templates, it's recommended to use the latter for better security.

Quick Start

Here's a simple example of using the template engine, as shown below:

go
package main

import (
  "fmt"
  "os"
  "text/template"
)

func main() {
  tmpl := `This is the first template string, {{ .message }}`

  te, err := template.New("texTmpl").Parse(tmpl)
  if err != nil {
    fmt.Println(err)
    return
  }

  data := map[string]any{
    "message": "hello world!",
  }
  execErr := te.Execute(os.Stdout, data)
  if execErr != nil {
    fmt.Println(err)
  }
}

The output of the above code is:

This is the first template string, hello world!

In the example code, tmpl is a template string, and {{ .message }} in the string is a template parameter of the template engine. First, parse the template string through the *Template.Parse method:

go
func (t *Template) Parse(text string) (*Template, error)

After successful parsing, use the *Template.Execute method to apply the data to the template, and finally output it to the passed Writer, which is os.Stdout.

go
func (t *Template) Execute(wr io.Writer, data any) error

In future uses of the template engine, it's basically these three steps:

  1. Get the template
  2. Parse the template
  3. Apply the data to the template

As you can see, using the template engine is quite simple. What's slightly more complex is the template syntax of the template engine, which is the main content of this article.

Template Syntax

Parameters

Go uses two pairs of curly braces {{ }} to represent a template parameter in the template. Use . to represent the root object, which is the passed data. Just like accessing a type's member variable, you can access the corresponding value by connecting the variable name with the . symbol in the template, for example:

{{ .data }}

This assumes that a member variable with the same name exists; otherwise, an error will be reported. For the passed data, it's generally a struct or map, but it can also be a basic type, such as a number or string. In this case, the root object represented by . is itself. Inside the curly braces, you don't have to access the root object to get the value; it can also be a literal of a basic type, for example:

{{ 1 }}
{{ 3.14 }}
{{ "jack" }}

Regardless of the type, it will ultimately obtain its string representation through fmt.Sprintf("%s", val). See the example below.

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ . }}\n"

  datas := []any{
    "hello world!",
    6379,
    3.1415926,
    []any{1, "2*2", 3.6},
    map[string]any{"data": "hello world!"},
    struct {
      Data string
    }{Data: "hello world!"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

func ExecTmpl(writer io.Writer, tmpl string, data any) error {
  parsedTmpl, err := template.New("template").Parse(tmpl)
  if err != nil {
    return err
  }
  return parsedTmpl.Execute(writer, data)
}

Output:

data-> hello world!
data-> 6379
data-> 3.1415926
data-> [1 2*2 3.6]
data-> map[data:hello world!]
data-> {hello world!}

As you can see, its output form is consistent with directly using fmt.Sprintf. For structs and maps, you can access their values by field name, as shown below:

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ .Data }}\n"

  datas := []any{
    map[string]any{"Data": "hello world!"},
    struct {
      Data string
    }{Data: "hello world!"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Output:

bash
data-> hello world!
data-> hello world!

For slices and map, although there's no specific syntax to access a value at a certain index, it can be achieved through function calls, as shown below:

go
func main() {
  out := os.Stdout

  tmpl := "data-> {{ index . 1}}\n"

  datas := []any{
    []any{"first", "second"},
    map[int]any{1: "first"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Output:

data-> second
data-> first

For multi-dimensional slices, you can access the value at the corresponding index in the following way, equivalent to s[i][j][k]:

{{ index . i j k }}

For nested structs or maps, you can use .k1.k2.k3 to access, for example:

{{ .person.father.name }}

When using template parameters, you can add - symbols before and after the parameter to eliminate whitespace before and after the parameter. See an example:

go
func main() {
  out := os.Stdout

  tmpl := `{{ .x }} {{ - .op - }} {{ .y }}`

  datas := []any{
    map[string]any{"x": "10", "op": ">", "y": "2"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Normally, the output should be 10 > 2, but since - symbols are added before and after the op parameter, the whitespace before and after it will be eliminated, so the actual output is:

10>2

Note that in the curly braces, the - symbol must be separated from the parameter by a space, meaning it must be in the format {{- . -}}. The reason why extra spaces are added on both sides in the example, written as {{ - . - }}, is purely because it looks better to me; there's actually no such syntax restriction.

Comments

The template syntax supports comments. Comments won't be generated in the final template. The syntax is as follows:

{{/* this is a comment */}}

The comment symbols /* and */ must be adjacent to the curly braces; there can't be other characters between them, otherwise it won't parse correctly. There's only one exception: when eliminating whitespace:

{{- /* this is a comment */ -}}

Variables

Variables can also be declared in templates. Use the $ symbol to indicate that this is a variable, and use := for assignment, just like in Go code. Examples:

{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
// Integer assignment
{{ $numer := 1 }}
// Float assignment
{{ $float := 1.234}}
// String assignment
{{ $name := "jack" }}

When using it later, access the variable's value by connecting the variable name with $, for example:

go
func main() {
  out := os.Stdout

  tmpl := `{{ $name := .name }} {{- $name }}`

  datas := []any{
    map[string]any{"name": "jack"},
  }

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, data)
    if err != nil {
      panic(err)
    }
  }
}

Output:

jack

Variables must be declared before use; otherwise, it will prompt undefined variable. They can also only be used within their scope.

Functions

The template's own syntax is actually not much; most functions are implemented through functions. The function call format is the function name followed by the parameter list, separated by spaces, as shown below:

{{ funcname arg1 arg2 arg3 ... }}

For example, the index function used earlier:

{{ index .s 1 }}

The eq function for comparing whether values are equal:

{{ eq 1 2 }}

Each *Template has a FuncsMap for recording function mappings:

go
type FuncMap map[string]any

When creating a template, get the default function mapping table from text/template.builtins. Below are all built-in functions:

Function NamePurposeExample
andAND operation{{ and true false }}
orOR operation{{ or true false }}
notNOT operation{{ not true }}
eqEqual to{{ eq 1 2 }}
neNot equal to{{ ne 1 2 }}
ltLess than{{ lt 1 2 }}
leLess than or equal to{{ le 1 2 }}
gtGreater than{{ gt 1 2 }}
geGreater than or equal to{{ ge 1 2 }}
lenReturn length{{ len .slice }}
indexGet element at specified index{{ index . 0 }}
sliceSlice, equivalent to s[1:2:3]{{ slice . 1 2 3 }}
htmlHTML escape{{ html .name }}
jsJS escape{{ js .name }}
printfmt.Sprint{{ print . }}
printffmt.Sprintf{{ printf "%s" .}}
printlnfmt.Sprintln{{ println . }}
urlqueryURL query escape{{ urlquery .query }}

Besides these, there's a special built-in function call, which is used to directly call a function passed in the data during the Execute period. For example, the following template:

{{ call .string 1024 }}

With the following data passed in:

go
map[string]any{
    "string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}

Then the template will generate:

1024: 2048

This is one way to customize functions, but it's usually recommended to use the *Template.Funcs method to add custom functions because the latter can act globally without binding to the root object.

go
func (t *Template) Funcs(funcMap FuncMap) *Template

The return value of a custom function generally has two values: the first is the return value needed, and the second is error. For example, with the following custom function:

go
template.FuncMap{
    "add": func(val any) (string, error) { return fmt.Sprintf("%v+1", val), nil },
}

Then use it directly in the template:

{{ add 1024 }}

The result is:

1024 + 1

Pipeline

This pipeline is different from chan. The official documentation calls it pipeline. Any operation that can produce data is called a pipeline. The following template operations all belong to pipeline operations:

{{ 1 }}
{{ eq 1 2 }}
{{ $name }}
{{ .name }}

Those familiar with Linux should know the pipe operator |. The template also supports this syntax. Pipeline operations frequently appear in templates, for example:

{{ $name := 1 }}{{ $name | print | printf "%s+1=?" }}

The result is:

1+1=?

It's also frequently used in subsequent with, if, and range statements.

With

The with statement can control the scope of variables and the root object. The format is as follows:

{{ with pipeline }}
  text
{{ end }}

with checks the value returned by the pipeline operation. If the value is empty, the text in the middle won't be generated. If you want to handle the empty case, you can use with else. The format is as follows:

{{ with pipeline }}
  text1
{{ else }}
  text2
{{ end }}

If the pipeline operation returns an empty value, the logic in the else block will be executed. Variables declared in a with statement are scoped only to the with statement. See the following example:

{{ $name := "mike" }}
{{ with $name := "jack"  }}
  {{- $name -}}
{{ end }}
{{- $name -}}

Its output is as follows. Obviously, this is because they are in different scopes, so they are two different variables.

jackmike

The with statement can also rewrite the root object within its scope, as follows:

{{ with .name }}
  name: {{- .second }}-{{ .first -}}
{{ end }}
age: {{ .age }}
address: {{ .address }}

Pass in the following data:

go
map[string]any{
    "name": map[string]any{
        "first":  "jack",
        "second": "bob",
    },
    "age":     1,
    "address": "usa",
}

Its output:

name:bob-jack
age: 1
address: usa

As you can see, inside the with statement, the root object . has become .name.

Conditionals

The format of conditional statements is as follows:

{{ if pipeline }}
  text1
{{ else if pipeline }}
  text2
{{ else }}
  text3
{{ end }}

Just like writing ordinary code, it's very easy to understand. Here are a few simple examples:

{{ if eq .lang "en" }}
  {{- .content.en -}}
{{ else if eq .lang "zh" }}
  {{- .content.zh -}}
{{ else }}
  {{- .content.fallback -}}
{{ end }}

Passed data:

go
map[string]any{
    "lang": "zh",
    "content": map[string]any{
        "en":       "hello, world!",
        "zh":       "你好,世界!",
        "fallback": "hello, world!",
    },
}

The template in the example decides how to display the content based on the passed language lang. Output result:

你好,世界!

Iteration

The format of iteration statements is as follows. The pipeline supported by range must be an array, slice, map, or channel.

{{ range pipeline }}
  loop body
{{ end }}

Use with else. When the length is 0, the content of the else block will be executed.

{{ range pipeline }}
  loop body
{{ else }}
  fallback
{{ end }}

In addition, it also supports operations like break and continue, for example:

{{ range pipeline }}
  {{ if pipeline }}
    {{ break }}
  {{ end }}
  {{ if pipeline }}
    {{ continue }}
  {{ end }}
  loop body
{{ end }}

Here's an example of iteration.

{{ range $index, $val := . }}
  {{- if eq $index 0 }}
    {{- continue -}}
  {{ end -}}
  {{- $index}}: {{ $val }}
{{ end }}

Passed data:

go
[]any{1, "2", 3.14},

Output:

1: 2
2: 3.14

Iterating over a map works the same way.

Nesting

A template can have multiple templates defined within it, for example:

{{ define "t1" }} t1 {{ end }}
{{ define "t2" }} t2 {{ end }}

These defined templates won't be generated in the final template unless a name is specified during loading or manually specified through the template statement.

func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error

For example, the following:

{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}

Pass in the following data:

go
map[string]any{
    "t1": map[string]any{"data": "template body 1"},
    "t2": map[string]any{"data": "template body 2"},
}

Code:

go
func main() {
  out := os.Stdout

  tmpl :=
    `{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}`

  datas := []any{
    map[string]any{
      "t1": map[string]any{"data": "template body 1"},
      "t2": map[string]any{"data": "template body 2"},
    },
  }

  name := "t1"

  for _, data := range datas {
    err := ExecTmpl(out, tmpl, name, data)
    if err != nil {
      panic(err)
    }
  }
}

func ExecTmpl(writer io.Writer, tmpl string, name string, data any) error {
  t := template.New("template")
  parsedTmpl, err := t.Parse(tmpl)
  if err != nil {
    return err
  }
  return parsedTmpl.ExecuteTemplate(writer, name, data)
}

Output:

template body 1

Or you can manually specify the template:

{{ define "t1" }}
    {{- with .t1 }}
      {{- .data -}}
    {{ end -}}
{{ end }}
{{ define "t2" }}
    {{- with .t2 }}
      {{- .data -}}
    {{ end}}
{{ end -}}
{{ template "t2" .}}

Then t2 will be loaded whether or not a template name is specified during parsing.

Association

Sub-templates just declare multiple named templates within a single template. Association links multiple named *Template from outside. Then reference the specified template through the template statement.

{{ template "templateName" pipeline}}

pipeline can specify the root object of the associated template according to your needs, or you can directly pass in the root object of the current template. See the following code example:

go
func main() {
  tmpl1 := `name: {{ .name }}`

  tmpl2 := `age: {{ .age }}`

  tmpl3 := `Person Info
{{template "t1" .}}
{{template "t2" .}}`

  t1, err := template.New("t1").Parse(tmpl1)
  if err != nil {
    panic(err)
  }

  t2, err := template.New("t2").Parse(tmpl2)
  if err != nil {
    panic(err)
  }

  t3, err := template.New("t3").Parse(tmpl3)
  if err != nil {
    panic(err)
  }

  if err := associate(t3, t1, t2); err != nil {
    panic(err)
  }

  err = t3.Execute(os.Stdout, map[string]any{
    "name": "jack",
    "age":  18,
  })
  if err != nil {
    panic(err)
  }
}

func associate(t *template.Template, ts ...*template.Template) error {
  for _, tt := range ts {
    _, err := t.AddParseTree(tt.Name(), tt.Tree)
    if err != nil {
      return err
    }
  }
  return nil
}

In the above code, t3 is associated with t1 and t2, using the *Template.AddParseTree method for association:

go
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)

The final template generation result is:

Person Info
name: jack
age: 18

Slots

Through the block statement, you can achieve effects similar to Vue slots. The purpose is to reuse a certain template. You'll know how to use it by looking at a usage case. Define slots in the t1 template:

Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} default content body {{ end }}

The block statement can have default content for the slot. When other templates use the slot later, they will override the default content. Reference the t1 template in the t2 template and use define to define the embedded content:

{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}

After associating the two templates, pass in the following data:

go
map[string]any{
    "name":    "jack",
    "age":     18,
    "address": "usa",
    "company": "google",
    "school":  "mit",
}

The final output is:

Basic Person Info
name: jack
age: 18
address: usa

school: mit

Template Files

In the template syntax examples, string literals are used as templates. In actual usage, templates are mostly placed in files.

go
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)

For example, template.ParseFs loads templates matching pattern from the specified file system. The following example uses embed.FS as the file system. Prepare three files:

txt
# person.txt
Basic Person Info
name: {{ .name }}
age: {{ .age }}
address: {{ .address }}
{{ block "slot" . }} {{ end }}

# student.txt
{{ template "person.txt" . }}
{{ define "slot" }}
school: {{ .school }}
{{ end }}

# employee.txt
{{ template "person.txt" . }}
{{ define "slot" }}
company: {{ .company }}
{{ end }}

Code:

go
import (
  "embed"
  "os"
  "text/template"
)

//go:embed *.txt
var fs embed.FS

func main() {
  data := map[string]any{
    "name":    "jack",
    "age":     18,
    "address": "usa",
    "company": "google",
    "school":  "mit",
  }

  t1, err := template.ParseFS(fs, "person.txt", "student.txt")
  if err != nil {
    panic(err)
  }

  t1.Execute(os.Stdout, data)

  t2, err := template.ParseFS(fs, "person.txt", "employee.txt")
  if err != nil {
    panic(err)
  }
  t2.Execute(os.Stdout, data)
}

Output:

Basic Person Info
name: jack
age: 18
address: usa

school: mit
Basic Person Info
name: jack
age: 18
address: usa

company: google

This is a very simple template file usage example. person.txt serves as a slot file, and the other two reuse its content and embed custom new content. You can also use the following two functions:

go
func ParseGlob(pattern string) (*Template, error)

func ParseFiles(filenames ...string) (*Template, error)

ParseGlob is based on wildcard matching, and ParseFiles is based on file names. They both use the local file system. If it's for displaying html files on the frontend, it's recommended to use the html/template package. Its API is completely consistent with text/template, but it has made security treatments for html, css, and js.

Golang by www.golangdev.cn edit