Skip to content

template

Официальная документация: template package - text/template - Go Packages

В повседневной разработке часто используется функция fmt.Sprintf для форматирования строк, но она подходит только для обработки небольших строк и требует использования спецификаторов формата для указания типов. Она не поддерживает именованные параметры и не справляется со сложными случаями. Именно эти проблемы решает шаблонизатор. Например, для статических HTML-страниц, отображаемых бэкендом, необходим шаблонизатор. В сообществе существует множество отличных сторонних библиотек шаблонизаторов, таких как pongo2, sprig, jet. Однако в этой статье речь пойдёт о встроенной библиотеке Go text/template. В реальной разработке обычно используется html/template, которая основана на первой и включает множество обработок безопасности для HTML. В большинстве случаев используется первая, но если речь идёт о шаблонах HTML, рекомендуется использовать вторую для большей безопасности.

Быстрый старт

Рассмотрим простой пример использования шаблонизатора:

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)
  }
}

Вывод:

This is the first template string, hello world!

В коде tmpl — это строка шаблона, а {{ .message }} — параметр шаблона. Сначала шаблон строки парсится методом *Template.Parse:

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

После успешного разбора метод *Template.Execute применяет данные data к шаблону и выводит результат в переданный Writer, в данном случае os.Stdout:

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

При использовании шаблонизатора обычно выполняются три шага:

  1. Получение шаблона
  2. Разбор шаблона
  3. Применение данных к шаблону

Использование шаблонизатора довольно просто. Более сложным является синтаксис шаблонов, который будет рассмотрен далее.

Синтаксис шаблонов

Параметры

В Go для обозначения параметра шаблона используются двойные фигурные скобки {{ }}. Точка . обозначает корневой объект, то есть переданные data. Как и при доступе к полям типа, через точку можно получить доступ к соответствующим значениям:

{{ .data }}

При условии, что поле с таким именем существует, иначе возникнет ошибка. Для data обычно используется структура или map, но могут быть и базовые типы, например, числа или строки. В этом случае . представляет само значение. Внутри фигурных скобок можно использовать не только корневой объект, но и литералы базовых типов:

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

Независимо от типа,最终 будет получено строковое представление через fmt.Sprintf("%s", val). Пример:

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)
}

Вывод:

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

Вывод аналогичен использованию fmt.Sprintf. Для структур и map можно обращаться к значениям по имени поля:

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)
    }
  }
}

Вывод:

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

Для срезов и map, хотя нет специального синтаксиса для доступа к конкретному индексу, можно использовать вызов функции:

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)
    }
  }
}

Вывод:

data-> second
data-> first

Для многомерных срезов можно использовать следующий синтаксис, эквивалентный s[i][j][k]:

{{ index . i j k }}

Для вложенных структур или map можно использовать цепочку .k1.k2.k3:

{{ .person.father.name }}

При использовании параметров шаблона можно добавить - до или после параметра для удаления пробелов:

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)
    }
  }
}

Обычно вывод был бы 10 > 2, но из-за - перед и после op пробелы удаляются, поэтому фактический вывод:

10>2

Следует отметить, что - должен быть отделён от параметра пробелом, т.е. формат должен быть {{- . -}}. В примере использован формат {{ - . - }} исключительно для визуального удобства, на самом деле такого синтаксического ограничения нет.

Комментарии

Синтаксис шаблонов поддерживает комментарии, которые не появляются в финальном выводе:

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

Символы /* и */ должны быть соседними с фигурными скобками, иначе парсинг не удастся. Исключение — удаление пробелов:

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

Переменные

В шаблонах можно объявлять переменные через символ $ и присваивать через :=, как в коде Go:

{{ $name := .Name }}
{{ $val := index . 1 }}
{{ $val := index .dict key }}
{{ $numer := 1 }}
{{ $float := 1.234}}
{{ $name := "jack" }}

Для доступа к значению переменной используется $ с именем переменной:

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)
    }
  }
}

Вывод:

jack

Переменные должны быть объявлены перед использованием, иначе возникнет ошибка undefined variable, и они действуют только в своей области видимости.

Функции

Синтаксис шаблонов сам по себе невелик, большинство функций реализовано через вызовы. Формат вызова: имя функции, затем аргументы, разделённые пробелами:

{{ funcname arg1 arg2 arg3 ... }}

Например, ранее использованная функция index:

{{ index .s 1 }}

Или функция eq для сравнения на равенство:

{{ eq 1 2 }}

Каждый *Template имеет FuncsMap для отображения функций:

go
type FuncMap map[string]any

При создании шаблона используется таблица функций по умолчанию из text/template.builtins. Ниже приведены все встроенные функции:

ФункцияОписаниеПример
andЛогическое И{{ and true false }}
orЛогическое ИЛИ{{ or true false }}
notЛогическое НЕ{{ not true }}
eqРавно{{ eq 1 2 }}
neНе равно{{ ne 1 2 }}
ltМеньше{{ lt 1 2 }}
leМеньше или равно{{ le 1 2 }}
gtБольше{{ gt 1 2 }}
geБольше или равно{{ ge 1 2 }}
lenДлина{{ len .slice }}
indexДоступ по индексу{{ index . 0 }}
sliceСрез, эквивалент s[1:2:3]{{ slice . 1 2 3 }}
htmlHTML-экранирование{{ html .name }}
jsJS-экранирование{{ js .name }}
printfmt.Sprint{{ print . }}
printffmt.Sprintf{{ printf "%s" .}}
printlnfmt.Sprintln{{ println . }}
urlqueryURL query экранирование{{ urlquery .query }}

Также есть специальная встроенная функция call для вызова функций, переданных в data во время выполнения:

{{ call .string 1024 }}

Данные:

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

Вывод в шаблоне:

1024: 2048

Это один из способов определения пользовательских функций, но обычно рекомендуется использовать метод *Template.Funcs, так как он действует глобально и не требует привязки к корневому объекту:

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

Пользовательские функции обычно возвращают два значения: нужное значение и error:

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

Использование в шаблоне:

{{ add 1024 }}

Результат:

1024 + 1

Конвейеры

Конвейер (pipeline) — это любая операция, генерирующая данные. Следующие операции являются конвейерами:

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

Те, кто знаком с Linux, знают оператор конвейера |. Шаблоны также поддерживают эту запись. Конвейеры часто используются в шаблонах:

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

Результат:

1+1=?

Конвейеры также часто используются в with, if, range.

with

Оператор with позволяет управлять областью видимости переменных и корневого объекта:

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

with проверяет значение конвейера: если оно пустое,中间的 text не генерируется. Для обработки пустого случая используется with else:

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

Если конвейер возвращает пустое значение, выполняется блок else. Переменные, объявленные в with, действуют только внутри with:

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

Вывод:

jackmike

Это связано с разными областями видимости — это две разные переменные.

Через with можно также переопределить корневой объект в области видимости:

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

Данные:

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

Вывод:

name:bob-jack
age: 1
address: usa

Внутри with корневой объект . стал .name.

Условия

Формат условных операторов:

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

Это очень похоже на обычный код. Пример:

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

Данные:

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

Шаблон выбирает способ отображения контента в зависимости от языка lang. Вывод:

你好,世界!

Итерация

Формат оператора итерации, range поддерживает массивы, срезы, map и channel:

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

С else: выполняется, если длина равна 0:

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

Также поддерживаются break и continue:

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

Пример итерации:

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

Данные:

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

Вывод:

1: 2
2: 3.14

Итерация по map работает аналогично.

Вложение

В одном шаблоне можно определить несколько шаблонов:

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

Определённые шаблоны не появляются в финальном выводе, если не указано имя при загрузке или не указано вручную через template:

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

Пример:

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

Данные:

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

Код:

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)
}

Вывод:

template body 1

Или можно вручную указать шаблон:

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

Тогда t2 будет загружен независимо от указания имени шаблона при разборе.

Связывание

Подшаблоны объявляют несколько именованных шаблонов внутри одного шаблона. Связывание соединяет несколько именованных *Template извне. Затем через оператор template ссылается на указанный шаблон:

{{ template "templateName" pipeline}}

pipeline позволяет указать корневой объект связываемого шаблона или передать корневой объект текущего шаблона. Пример:

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
}

В этом коде t3 связывает t1 и t2 через метод *Template.AddParseTree:

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

Финальный вывод:

Person Info
name: jack
age: 18

Слоты

Через оператор block можно реализовать эффект, подобный слотам Vue, для повторного использования шаблона. Пример: в шаблоне t1 определяется слот:

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

Оператор block определяет содержимое слота по умолчанию. При использовании слота другим шаблоном содержимое заменяется. В шаблоне t2 ссылается на t1 и определяет встраиваемое содержимое:

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

После связывания шаблонов передаются данные:

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

Финальный вывод:

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

school: mit

Файлы шаблонов

В примерах использовались строковые литералы как шаблоны, но на практике шаблоны обычно хранятся в файлах.

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

template.ParseFS загружает шаблоны из файловой системы, соответствующие pattern. Пример с embed.FS:

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 }}

Код:

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)
}

Вывод:

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

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

company: google

Это простой пример использования файлов шаблонов. person.txt служит базовым шаблоном, другие два переиспользуют его содержимое и добавляют собственное. Также можно использовать функции:

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

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

ParseGlob использует сопоставление по шаблону, ParseFiles — по именам файлов, обе работают с локальной файловой системой. Для HTML-файлов, отображаемых в браузере, рекомендуется использовать пакет html/template. Его API полностью совпадает с text/template, но включает обработки безопасности для html, css, js.

Golang by www.golangdev.cn edit