обзор
Текст вокруг нас как разработчиков программного обеспечения. Код — это текст, HTML — это текст, XNL / JSON / YAML / TOML — это текст, Markdown — это текст, CSV — это текст. Все эти текстовые форматы предназначены для людей и машин. Люди должны иметь возможность читать и редактировать текстовые форматы с помощью текстовых редакторов.
Но во многих случаях вам нужно генерировать текст в определенном формате. Вы можете конвертировать из одного формата в другой, создать свой собственный DSL, автоматически сгенерировать некоторый вспомогательный код или просто настроить электронное письмо с информацией для конкретного пользователя. Независимо от необходимости, Go более чем способен помочь вам на этом пути с его мощными шаблонами.
В этом руководстве вы узнаете о плюсах и минусах шаблонов Go и о том, как использовать их для мощной генерации текста.
Что такое шаблоны Go?
Шаблоны Go — это объекты, которые управляют некоторым текстом с помощью специальных заполнителей, называемых действиями, которые заключены в двойные фигурные скобки: {{ some action }}
. Когда вы выполняете шаблон, вы предоставляете ему структуру Go, которая содержит данные, необходимые для заполнителей.
Вот быстрый пример, который генерирует анекдоты о стуке. Шутка тук-тук имеет очень строгий формат. Единственными вещами, которые меняются, является личность молотка и изюминка.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
package main
import (
«text/template»
«os»
)
type Joke struct {
Who string
Punchline string
}
func main() {
t := template.New(«Knock Knock Joke»)
text := `Knock Knock\nWho’s there?
{{.Who}}
{{.Who}} who?
{{.Punchline}}
`
t.Parse(text)
jokes := []Joke{
{«Etch», «Bless you!»},
{«Cow goes», «No, cow goes moo!»},
}
for _, joke := range jokes {
t.Execute(os.Stdout, joke)
}
}
Output:
Knock Knock
Who’s there?
Etch
Etch who?
Bless you!
Knock Knock
Who’s there?
Cow goes
Cow goes who?
No, cow goes moo!
|
Понимание действий шаблона
Синтаксис шаблона очень мощный, и он поддерживает такие действия, как средства доступа к данным, функции, конвейеры, переменные, условия и циклы.
Средства доступа к данным
Средства доступа к данным очень просты. Они просто извлекают данные из начала структуры. Они также могут углубляться во вложенные структуры:
01
02
03
04
05
06
07
08
09
10
11
12
|
func main() {
family := Family{
Father: Person{«Tarzan»},
Mother: Person{«Jane»},
ChildrenCount: 2,
}
t := template.New(«Father»)
text := «The father’s name is {{.Father.Name}}»
t.Parse(text)
t.Execute(os.Stdout, family)
}
|
Если данные не являются структурой, вы можете использовать просто {{.}}
Для прямого доступа к значению:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
|
func main() {
t := template.New(«»)
t.Parse(«Anything goes: {{.}}\n»)
t.Execute(os.Stdout, 1)
t.Execute(os.Stdout, «two»)
t.Execute(os.Stdout, 3.0)
t.Execute(os.Stdout, map[string]int{«four»: 4})
}
Output:
Anything goes: 1
Anything goes: two
Anything goes: 3
Anything goes: map[four:4]
|
Позже мы увидим, как обращаться с массивами, срезами и картами.
функции
Функции действительно улучшают то, что вы можете делать с шаблонами. Существует множество глобальных функций, и вы даже можете добавить функции, специфичные для шаблона. Полный список глобальных функций доступен на веб-сайте Go .
Вот пример того, как использовать функцию printf
в шаблоне:
01
02
03
04
05
06
07
08
09
10
|
func main() {
t := template.New(«»)
t.Parse(`Keeping just 2 decimals of π: {{printf «%.2f» .}}
`)
t.Execute(os.Stdout, math.Pi)
}
Output:
Keeping just 2 decimals of π: 3.14
|
Трубопроводы
Конвейеры позволяют применять несколько функций к текущему значению. Объединение различных функций значительно расширяет способы, с помощью которых вы можете нарезать и нарезать свои значения.
В следующем коде я объединяю три функции. Сначала функция вызова выполняет передачу функции в Execute()
. Затем функция len
возвращает длину результата входной функции, которая в данном случае равна 3. Наконец, функция printf
печатает количество элементов.
01
02
03
04
05
06
07
08
09
10
|
func main() {
t := template.New(«»)
t.Parse(`{{ call . | len | printf «%d items» }}
`)
t.Execute(os.Stdout, func() string { return «abc» })
}
Output:
3 items
|
переменные
Иногда вы хотите повторно использовать результат сложного конвейера несколько раз. С помощью шаблонов Go вы можете определить переменную и использовать ее столько раз, сколько захотите. В следующем примере извлекаются имя и фамилия из входной структуры, заключаются в кавычки и сохраняются в переменных $F
и $L
Затем он отображает их в обычном и обратном порядке.
Еще одна хитрость в том, что я передаю анонимную структуру в шаблон, чтобы сделать код более лаконичным и избежать загромождения его типами, которые используются только в одном месте.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
|
func main() {
t := template.New(«»)
t.Parse(`{{ $F := .FirstName | printf «%q»}}
{{ $L := .LastName |
Normal: {{$F}} {{$L}}
Reverse: {{$L}} {{$F}}`
)
t.Execute(os.Stdout, struct {
FirstName string
LastName string
}{
«Gigi»,
«Sayfan»,
})
}
Output:
Normal: «Gigi» «Sayfan»
Reverse: «Sayfan» «Gigi»
|
Conditionals
Но давайте не будем останавливаться на достигнутом. Вы даже можете иметь условия в своих шаблонах. Существует действие if-end
действие if-else-end
. Предложение if отображается, если выходные данные условного конвейера не пусты:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
func main() {
t := template.New(«»)
t.Parse(`{{ if . -}} {{ . }} {{ else }}
No data is available {{ end }}`
)
t.Execute(os.Stdout, «42»)
t.Execute(os.Stdout, «»)
}
Output:
42
No data is available
|
Обратите внимание, что предложение else вызывает новую строку, а текст «Нет данных» имеет значительный отступ.
Loops
В шаблонах Go тоже есть петли. Это очень полезно, когда ваши данные содержат фрагменты, карты или другие итерации. Объектом данных для цикла может быть любой итеративный объект Go, например массив, фрагмент, карта или канал. Функция range позволяет перебирать объект данных и создавать выходные данные для каждого элемента. Давайте посмотрим, как перебрать карту:
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
|
func main() {
t := template.New(«»)
e := `Name,Scores
{{range $k, $v := .}}{{$k}}
{{range $s := $v}},{{$s}}{{end}}
{{end}}
`
t.Parse(e)
t.Execute(os.Stdout, map[string][]int{
«Mike»: {88, 77, 99},
«Betty»: {54, 96, 78},
«Jake»: {89, 67, 93},
})
}
Output:
Name,Scores
Betty,54,96,78
Jake,89,67,93
Mike,88,77,99
|
Как вы можете видеть, ведущий пробел все еще является проблемой. Я не смог найти достойного способа решения этой проблемы в синтаксисе шаблона. Это потребует постобработки. Теоретически, вы можете поместить тире, чтобы обрезать пропуски перед или после действий, но это не работает при наличии range
.
Текстовые шаблоны
Текстовые шаблоны реализованы в пакете text / template . В дополнение ко всему, что мы видели до сих пор, этот пакет может также загружать шаблоны из файлов и создавать несколько шаблонов, используя действие шаблона. Сам объект Template имеет много методов для поддержки таких расширенных вариантов использования:
- ParseFiles ()
- ParseGlob ()
- AddParseTree ()
- Клон ()
- DefinedTemplates ()
- Delims ()
- ExecuteTemplate ()
- Funcs ()
- Погляди()
- Вариант ()
- Шаблоны ()
Из-за нехватки места я не буду вдаваться в подробности (возможно, в другой урок).
HTML-шаблоны
Шаблоны HTML определены в пакете html / template . Он имеет тот же интерфейс, что и пакет текстового шаблона, но он предназначен для генерации HTML, который защищен от внедрения кода. Это делается путем тщательной очистки данных перед их внедрением в шаблон. Рабочим предположением является то, что авторам шаблонов доверяют, но данные, предоставленные шаблону, нельзя доверять.
Это важно. Если вы автоматически применяете шаблоны, полученные из ненадежных источников, пакет html / template вас не защитит. Вы несете ответственность за проверку шаблонов.
Давайте посмотрим на разницу между выводом text/template
и html/template
. При использовании текста / шаблона легко внедрить код JavaScript в сгенерированный вывод.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
|
package main
import (
«text/template»
«os»
)
func main() {
t, _ := template.New(«»).Parse(«Hello, {{.}}!»)
d := «<script>alert(‘pawned!’)</script>»
t.Execute(os.Stdout, d)
}
Output:
Hello, <script>alert(‘pawned!’)</script>!
|
Но импорт html/template
вместо text/template
предотвращает эту атаку, избегая тегов скрипта и скобок:
1
|
Hello, <script>alert('pwened!')</script>!
|
Работа с ошибками
Есть два типа ошибок: ошибки синтаксического анализа и ошибки выполнения. Функция Parse()
анализирует текст шаблона и возвращает ошибку, которую я игнорировал в примерах кода, но в рабочем коде вы хотите отлавливать эти ошибки заранее и исправлять их.
Если вам нужен быстрый и грязный выход, то метод Must()
принимает выходные данные метода, который возвращает (*Template, error)
как Clone()
, Parse()
или ParseFiles()
— и паникует, если ошибка не ноль. Вот как вы проверяете явную ошибку разбора:
01
02
03
04
05
06
07
08
09
10
11
12
13
|
func main() {
e := «I’m a bad template, }}{{«
_, err := template.New(«»).Parse(e)
if err != nil {
msg := «Failed to parsing: ‘%s’.\nError: %v\n»
fmt.Printf(msg, e, err)
}
}
Output:
Failed to parse template: ‘I’m a bad template, }}{{‘.
Error: template: :1: unexpected unclosed action in command
|
Использование Must()
просто паникует, если что-то не так с шаблоном:
1
2
3
4
5
6
7
8
|
func main() {
e := «I’m a bad template, }}{{«
template.Must(template.New(«»).Parse(e))
}
Output:
panic: template: :1: unexpected unclosed action in command
|
Другой вид ошибки — ошибка выполнения, если предоставленные данные не соответствуют шаблону. Опять же, вы можете проверить явно или использовать Must()
для паники. В этом случае я рекомендую проверить и установить механизм восстановления.
Обычно нет необходимости разрушать всю систему только потому, что входные данные не соответствуют требованиям. В следующем примере шаблон ожидает поле с именем Name
в структуре данных, но я предоставляю структуру с полем с именем FullName
.
01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
|
func main() {
e := «There must be a name: {{.Name}}»
t, _ := template.New(«»).Parse(e)
err := t.Execute(
os.Stdout,
struct{ FullName string }{«Gigi Sayfan»},
)
if err != nil {
fmt.Println(«Fail to execute.», err)
}
}
Output:
There must be a name: Fail to execute.
template: :1:24: executing «» at <.Name>:
can’t evaluate field Name in type struct { FullName string }
|
Вывод
Go имеет мощную и сложную систему шаблонов. Он используется с большим эффектом во многих крупных проектах, таких как Kubernetes и Hugo. Пакет html / template обеспечивает безопасное, промышленное средство для дезинфекции результатов веб-систем. В этом уроке мы рассмотрели все основы и некоторые промежуточные варианты использования.
В пакетах шаблонов есть еще более продвинутые функции, ожидающие разблокировки. Поиграйте с шаблонами и включите их в свои программы. Вы будете приятно удивлены, насколько лаконичным и читабельным выглядит ваш код для генерации текста.