Golang на «своем борту» несет мощный универсальный шаблонизатор, позволяющий динамически формировать вывод как текстовой информации (например, электронное письмо, документ или просто консольная команда), так и целых веб-страниц.
Шаблонизация в Go состоит из двух базовых пакетов — у каждого свое предназначение:
text/template
html/template
Важно отметить, что оба пакета имеют абсолютно идентичный интерфейс, однако второй автоматически защищает вывод HTML от определенных видов атак — например, от injections.
Преобразование Go-шаблона в итоговый вывод выполняется за счет применения к шаблону соответствующей структуры данных. Входной текст для Golang шаблона представляется в любом формате в кодировке UTF-8.
Используемый шаблон, как правило, привязывается к определенной структуре данных (например, struct), данные из которой будут появляться внутри шаблона.
Поэтому, любой шаблон формально состоит 3 типов базовых сущностей, которые «вынимают» из него нужные переменные и подставляют в вывод.
Это фрагменты текста, заключенные в фигурные скобки {{ }}
, в которых выполняется вычисление или подстановка некоторых данных. Именно за счет них текст внутри шаблона по своему наполнению становится динамическим.
Действиями можно считать как простую подстановку переменной, так и выполнение циклов или функций, который формируют итоговый текст. Все они непосредственно управляют тем, как будет выглядеть окончательный результат.
К условиям относятся классические конструкции if-else
, которые используются непосредственно внутри шаблона. Благодаря условиям можно добавлять или убирать из конечного вывода целые текстовые блоки, что существенно увеличивает возможности шаблонизации и гибкость генерации контента.
Внутри шаблона можно выполнять классические циклы, выводя множество однотипных блоков, но с разной ключевой информацией.
Непосредственно в самом Go есть 3 наиболее часто используемые функции для управления шаблонами:
New
. Создает новый шаблон, который впоследствии нужно будет определить.Parse
.
Анализирует переданную строку, содержащую текст шаблона, после чего возвращает уже готовый к использованию шаблон.Execute
. Выполняет готовый шаблон (который прошел этап парсинга), применяя к нему указанную структуру данных, после чего записывает результат в заданную переменнуюКстати, еще существует функция ParseFiles
для обработки не строки с содержимым шаблона, а целых файлов.
Следующий код демонстрирует, как использование этих базовых шаблонных функций выглядит в самом тривиальном случае:
package main
// помимо шаблонизатора подключаем os, который предоставляет независимый от платформы интерфейс для взаимодействия с операционной системой. В нашем случае он по понадобиться для вывода результата выполнения шаблона в консоль
import (
"os"
"text/template"
)
// определяем структуру, данные которой будут подставляться в шаблон
type Person struct {
Name string
Age int
}
func main() {
some_person := Person{"Alex", 32} // экземпляр ранее определенной структуры
some_template := "This is {{ .Name }} and he is {{ .Age }} years old" // текстовое содержимое самого шаблона с проставленными внутри действиями (actions), заключенными в фигурные скобки
// создаем новый шаблон и парсим его содержимое, тем самым подготавливая его к дальнейшему использованию
ready_template, err := template.New("test").Parse(some_template)
// проверяем на наличие ошибок (nil означает ничего, как null в C)
if err != nil {
panic(err) // останавливаем выполнение и выводим содержимое ошибки
}
// выполняем шаблон и выводим его в консоль приложения
err = ready_template.Execute(os.Stdout, some_person) // ВЫВОД: This is Alex and he is 32 years old
// снова проверяем на наличие ошибок
if err != nil {
panic(err) // останавливаем выполнение и выводим содержимое ошибки
}
}
«Скомпилированный» с помощью функции Parse
шаблон можно использовать повторно, но уже с данными из другой структуры. Например, вы могли бы продолжить функцию main
из кода выше:
// здесь идет ранее написанный код
...
another_person := Person{"Maks", 27} // создаем другой экземпляр структуры
err = ready_template.Execute(os.Stdout, another_person)
}
Обратите внимание, что внутри шаблона в фигурных скобках указываются переменные структуры, которая была передана в шаблон при его выполнении. При этом имя переменной указывается через точку, а название самой структуры как бы опускается:
This is {{ .Name }} and he is {{ .Age }} years old
Кстати, внутри шаблона можно напрямую обратиться к данным, которые были переданы при выполнении:
package main
import (
"os"
"text/template"
)
func main() {
some_template := "Тут обошлось {{ . }}"
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, "без данных, просто текст") // ВЫВОД: Тут обошлось без данных, просто текст
}
В самом трививальном случае шаблон может просто выводить статичный текст не используя никаких дополнительных данных:
import (
"os"
"text/template"
)
...
some_template := "Просто обычный текст"
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Просто обычный текст
Обычный статический текст можно усложнить дополнительными данными:
import (
"os"
"text/template"
)
...
some_template := "Не совсем обычный текст с {{ \“дополнительными\” }} данными" // не забываем также экранировать двойные кавычки
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Не совсем обычный текст с дополнительными данными
Кстати, можно дополнительно перед фигурными скобками и после них указывать маркеры обрезки:
...
some_template := "Не совсем обычный текст с {{- \“дополнительными\” -}} данными"
...
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Не совсем обычный текст сдополнительнымиданными
// вывод выше не опечатка — были убраны пробелы перед и после слова “дополнительными”
Маркеры обрезки убирают не только один, но и несколько пробелов по краям того текста, который получается после выполнения кода внутри фигурных скобок — как с внутренней стороны, так и с внешней.
В отличие от текста, который нужно указывать в кавычках, числа автоматически подставляются в вывод:
...
some_template := "Возможно этот код писало {{ 5 }} человек. Но это не точно..."
...
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Возможно этот код писало 5 человек. Но это не точно...
Как и с текстом, с числами тоже можно использовать маркеры обрезки:
...
some_template := "Возможно этот код писало {{- 5 }} человек. Но это не точно..."
...
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Возможно этот код писало5 человек. Но это не точно...
Golang позволяет определять особые переменные, доступные только внутри шаблона.
Как и в самом Go, переменная сначала определяется с указанием имени и значения, а уже потом используется. Определение внутренней переменной происходит с помощью знака доллара:
package main
import (
"os"
"text/template"
)
func main() {
some_template := "Сначала определяем переменную {{- $some_variable :=`Привет, я переменная` }}, а потом используем ее: \"{{ $some_variable }}\""
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, ”без данных”) // ВЫВОД: Сначала определяем переменную, а потом используем ее: "Привет, я переменная"
}
Обратите внимание, что для доступа к переменной используется не точка, а знак доллара, ведь эта переменная не имеет отношения к какой либо структуре данных Go, а лишь определена внутри самого шаблона.
В шаблонах Go есть возможность ветвления на основе какой либо логики за счет классических для всех языков программирования условных операторов if
/ else
:
package main
import (
"os"
"text/template"
)
func main() {
some_template := "{{ if eq . `hello` -}} Привет! {{ else -}} Пока! {{ end }}" // после каждого условия используем маркер обрезки, чтобы убрать пробел вначале вывода
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, "hello") // ВЫВОД: Привет!
}
В этом примере используется функция eq (equal), которая сравнивает переданное в шаблон значение (к которому мы обращаемся через точку) со строкой hello.
Также обратите внимание, что каждое ветвление завершается оператором end
.
На самом деле можно опустить сравнение строк, а сразу передавать булеву переменную, делая код лаконичнее:
package main
import (
"os"
"text/template"
)
func main() {
some_template := "{{ if . -}} Привет! {{ else -}} Пока! {{ end }}"
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, false) // ВЫВОД: Пока!
}
Как правило, шаблоны используются для вывода множества однотипных данных, количество которых меняются от вывода к выводу. Именно для этого и нужны циклы:
package main
import (
"os"
"text/template"
)
func main() {
some_list := []string{ "First", "Second", "Third" }
some_template := "Посчитаем по порядку: {{ range .}}{{.}}, {{ end }}"
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, some_list) // ВЫВОД: Посчитаем по порядку: First, Second, Third,
}
В этом примере есть одна проблема — последний элемент списка выводит после себя лишнюю запятую с пробелом. Поэтому код выше можно модернизировать, добавив соответствующую проверку — если элемент последний в списке, то запятая с пробелом не ставится:
package main
import (
"os"
"text/template"
)
func main() {
some_list := []string{ "First", "Second", "Third" }
some_template := "Посчитаем по порядку: {{ range $index, $element := .}}{{ if $index }}, {{ end }}{{$element}}{{ end }}"
ready_template, err := template.New("test").Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, some_list) // ВЫВОД: Посчитаем по порядку: First, Second, Third
}
В этом примере добавляются две новые переменные, которые обновляются каждую итерацию цикла — индекс и значение элемента.
Перед каждым элементом мы выводим запятую с пробелом, но только в том случае, когда индекс ($index
) не равен нулю.
Внутри шаблонов можно вызывать свои собственные «кастомные» функции, выполняющие различные операции над переданными аргументами.
Однако, перед непосредственным использованием их нужно предварительно объявить и задекларировать в шаблоне:
package main
import (
"os"
"text/template"
)
func manipulate(first_arg, second_arg int) int {
return first_arg + second_arg
}
func main() {
some_list := []int{1, 2, 3 }
some_template := "Сложения индекса и элемента по порядку: {{ range $index, $element := .}}{{ if $index }}, {{ end }}{{$index}} + {{$element}} = {{ do_manipulation $index $element }}{{ end }}"
ready_template, err := template.New("test").Funcs(template.FuncMap{"do_manipulation": manipulate}).Parse(some_template)
if err != nil { panic(err) }
ready_template.Execute(os.Stdout, some_list) // ВЫВОД: Сложения индекса и элемента по порядку: 0 + 1 = 1, 1 + 2 = 3, 2 + 3 = 5
}
В этом примере мы нарочно переименовали Go-функцию manipulate
внутри шаблона в do_manipulation
— если функционал языка позволяет, то почему бы это не сделать?
В любом случае, могли бы использовать и оригинальное название функции, просто продублировав его:
ready_template, err := template.New("test").Funcs(template.FuncMap{"manipulate": manipulate}).Parse(some_template)
Как уже было сказано ранее, в Go есть дополнительный пакет для работы с шаблонами на HTML — html/template
.
Использование этого пакета, в отличие от стандартного text/template
, защищает приложения от атак межсайтового скриптинга (XSS), поскольку Go предотвращает ввод каких-либо данных во время рендеринга.
Соответственно, импорт пакетов немного видоизменяется:
import (
"html/template"
"net/http"
)
Пакет net/http
потребуется для запуска HTTP-сервера на локальной машине, который необходим для теста дальнейшего примера.
Лучшей практикой было бы хранение шаблона в отдельном файле. В нашем случае мы создадим файл с оригинальным расширением .html
, хотя в своих проектах вы можете использовать абсолютно любое расширение — Golang не накладывает никаких ограничений.
Кстати, файл назовем классическим именем — index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<h1>{{ .Tittle }}</h1>
<p> {{ .Text }} </p>
</body>
</html>
Вы можете заметить, что мы заранее указали соответствующие названия двух переменных — их значения будут получены из передаваемый в шаблон структуры.
Теперь напишем минимальный Golang-код, запускающий HTTP-сервер и отправляющий результат выполнения шаблона в качестве ответа на любые запросы к серверу:
package main
import (
"os"
"html/template"
"net/http"
)
// объявляем структуру, которая будет хранить информацию для генерации шаблона
type Content struct {
Title string
Text string
}
// функция, которая будет обрабатывать HTTP-запросы к серверу
func generateResponse(writer http.ResponseWriter, request *http.Request) {
if request.Method == "GET" {
some_template, _ = template.ParseFiles("index.html")
some_content := Content {
Title: "Это заголовок",
Text: "Это текст",
}
err := some_template.Execute(writer, some_content) // выполняем шаблон, записывая его вывод в переменную ответа сервера
if err != nil {
panic(err)
}
}
func main() {
// запускаем HTTP-сервер, указывая ранее созданную функцию обработки запросов в качестве аргумента
http.HandleFunc("/", generateResponse)
err := http.ListenAndServe("localhost:8080", nil)
if err != nil {
log.Fatalln("Случилась какая-то ошибка:", err)
}
}
Итак. Язык программирования Go предлагает встроенную поддержку для создания динамического контента или для отображения настраиваемого вывода — шаблоны.
Поэтому, в этой статье были показаны базовые функции шаблонов, которые позволяют управлять данными, динамически изменяя их согласно некоторому паттерну — описанию самого шаблона.
Сама реализация предполагает несколько вариантов использования:
text/template
html/template
Помните, что любой шаблон проходит 3 стадии формирования, каждая из которых обеспечивается соответствующей функцией:
New
. Создание шаблонаParse
. Анализ (парсинг) шаблона.Execute
. Выполнение шаблона. При этом эта стадия может иметь неограниченное число повторений.Кстати, подробную информацию о доступных функциях и способах использования можно найти в официальной документации Golang — на страницах text/template и html/template соответственно.