Статьи

Генерация текста с помощью шаблонов Go

Текст вокруг нас как разработчиков программного обеспечения. Код — это текст, HTML — это текст, XNL / JSON / YAML / TOML — это текст, Markdown — это текст, CSV — это текст. Все эти текстовые форматы предназначены для людей и машин. Люди должны иметь возможность читать и редактировать текстовые форматы с помощью текстовых редакторов.

Но во многих случаях вам нужно генерировать текст в определенном формате. Вы можете конвертировать из одного формата в другой, создать свой собственный DSL, автоматически сгенерировать некоторый вспомогательный код или просто настроить электронное письмо с информацией для конкретного пользователя. Независимо от необходимости, 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»

Но давайте не будем останавливаться на достигнутом. Вы даже можете иметь условия в своих шаблонах. Существует действие 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 вызывает новую строку, а текст «Нет данных» имеет значительный отступ.

В шаблонах 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 / 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, &lt;script&gt;alert(&#39;pwened!&#39;)&lt;/script&gt;!

Есть два типа ошибок: ошибки синтаксического анализа и ошибки выполнения. Функция 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 обеспечивает безопасное, промышленное средство для дезинфекции результатов веб-систем. В этом уроке мы рассмотрели все основы и некоторые промежуточные варианты использования.

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