template
Официальная документация: template package - text/template - Go Packages
В повседневной разработке часто используется функция fmt.Sprintf для форматирования строк, но она подходит только для обработки небольших строк и требует использования спецификаторов формата для указания типов. Она не поддерживает именованные параметры и не справляется со сложными случаями. Именно эти проблемы решает шаблонизатор. Например, для статических HTML-страниц, отображаемых бэкендом, необходим шаблонизатор. В сообществе существует множество отличных сторонних библиотек шаблонизаторов, таких как pongo2, sprig, jet. Однако в этой статье речь пойдёт о встроенной библиотеке Go text/template. В реальной разработке обычно используется html/template, которая основана на первой и включает множество обработок безопасности для HTML. В большинстве случаев используется первая, но если речь идёт о шаблонах HTML, рекомендуется использовать вторую для большей безопасности.
Быстрый старт
Рассмотрим простой пример использования шаблонизатора:
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:
func (t *Template) Parse(text string) (*Template, error)После успешного разбора метод *Template.Execute применяет данные data к шаблону и выводит результат в переданный Writer, в данном случае os.Stdout:
func (t *Template) Execute(wr io.Writer, data any) errorПри использовании шаблонизатора обычно выполняются три шага:
- Получение шаблона
- Разбор шаблона
- Применение данных к шаблону
Использование шаблонизатора довольно просто. Более сложным является синтаксис шаблонов, который будет рассмотрен далее.
Синтаксис шаблонов
Параметры
В Go для обозначения параметра шаблона используются двойные фигурные скобки {{ }}. Точка . обозначает корневой объект, то есть переданные data. Как и при доступе к полям типа, через точку можно получить доступ к соответствующим значениям:
{{ .data }}При условии, что поле с таким именем существует, иначе возникнет ошибка. Для data обычно используется структура или map, но могут быть и базовые типы, например, числа или строки. В этом случае . представляет само значение. Внутри фигурных скобок можно использовать не только корневой объект, но и литералы базовых типов:
{{ 1 }}
{{ 3.14 }}
{{ "jack" }}Независимо от типа,最终 будет получено строковое представление через fmt.Sprintf("%s", val). Пример:
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 можно обращаться к значениям по имени поля:
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, хотя нет специального синтаксиса для доступа к конкретному индексу, можно использовать вызов функции:
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 }}При использовании параметров шаблона можно добавить - до или после параметра для удаления пробелов:
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" }}Для доступа к значению переменной используется $ с именем переменной:
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 для отображения функций:
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 }} |
html | HTML-экранирование | {{ html .name }} |
js | JS-экранирование | {{ js .name }} |
print | fmt.Sprint | {{ print . }} |
printf | fmt.Sprintf | {{ printf "%s" .}} |
println | fmt.Sprintln | {{ println . }} |
urlquery | URL query экранирование | {{ urlquery .query }} |
Также есть специальная встроенная функция call для вызова функций, переданных в data во время выполнения:
{{ call .string 1024 }}Данные:
map[string]any{
"string": func(val any) string { return fmt.Sprintf("%v: 2048", val) },
}Вывод в шаблоне:
1024: 2048Это один из способов определения пользовательских функций, но обычно рекомендуется использовать метод *Template.Funcs, так как он действует глобально и не требует привязки к корневому объекту:
func (t *Template) Funcs(funcMap FuncMap) *TemplateПользовательские функции обычно возвращают два значения: нужное значение и error:
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 }}Данные:
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 }}Данные:
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 }}Данные:
[]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 -}}Данные:
map[string]any{
"t1": map[string]any{"data": "template body 1"},
"t2": map[string]any{"data": "template body 2"},
}Код:
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 позволяет указать корневой объект связываемого шаблона или передать корневой объект текущего шаблона. Пример:
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:
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 }}После связывания шаблонов передаются данные:
map[string]any{
"name": "jack",
"age": 18,
"address": "usa",
"company": "google",
"school": "mit",
}Финальный вывод:
Basic Person Info
name: jack
age: 18
address: usa
school: mitФайлы шаблонов
В примерах использовались строковые литералы как шаблоны, но на практике шаблоны обычно хранятся в файлах.
func ParseFS(fsys fs.FS, patterns ...string) (*Template, error)template.ParseFS загружает шаблоны из файловой системы, соответствующие pattern. Пример с embed.FS:
# 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 }}Код:
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 служит базовым шаблоном, другие два переиспользуют его содержимое и добавляют собственное. Также можно использовать функции:
func ParseGlob(pattern string) (*Template, error)
func ParseFiles(filenames ...string) (*Template, error)ParseGlob использует сопоставление по шаблону, ParseFiles — по именам файлов, обе работают с локальной файловой системой. Для HTML-файлов, отображаемых в браузере, рекомендуется использовать пакет html/template. Его API полностью совпадает с text/template, но включает обработки безопасности для html, css, js.
