Описание пакета html/template в Go
Пакет html/template - шаблоны HTML с защитой от инъекций
Пакет html/template
реализует шаблоны, управляемые данными, для генерации HTML-вывода, защищённого от внедрения кода. Он предоставляет тот же интерфейс, что и text/template
, и должен использоваться вместо него, когда выводом является HTML.
Данная документация фокусируется на функциях безопасности пакета. Информацию о программировании самих шаблонов см. в документации text/template
.
Введение
Этот пакет оборачивает text/template
, позволяя использовать его API шаблонов для безопасного разбора и выполнения HTML-шаблонов.
Пример:
tmpl, err := template.New("name").Parse(...)
// Проверка ошибок опущена
err = tmpl.Execute(out, data)
Если операция успешна, tmpl
будет защищён от инъекций. В противном случае err
будет содержать ошибку, как описано в ErrorCode.
HTML-шаблоны обрабатывают значения данных как обычный текст, который должен быть закодирован для безопасного встраивания в HTML-документ. Экранирование является контекстно-зависимым, поэтому действия могут появляться в JavaScript, CSS и URI-контекстах.
Модель безопасности, используемая этим пакетом, предполагает, что авторы шаблонов являются доверенными, а параметр данных Execute
- нет. Более подробная информация приведена ниже.
import "text/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
Вывод
Hello, <script>alert('you have been pwned')</script>!
но контекстное автоэкранирование в html/шаблоне
import "html/template"
...
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
err = t.ExecuteTemplate(out, "T", "<script>alert('you have been pwned')</script>")
создает безопасный, экранированный HTML-вывод
Hello, <script>alert('you have been pwned')</script>!
Контексты
Этот пакет понимает HTML, CSS, JavaScript и URI. Он добавляет функции очистки к каждому простому конвейеру действий, поэтому, рассмотрим пример
<a href="/search?q={{.}}">{{.}}</a>
Во время разбора каждый {{.}} перезаписывается для добавления экранирующих функций по мере необходимости. В этом случае это становится
<a href="/search?q={{. | urlescaper | attrescaper}}">{{. | htmlescaper}}</a>
где urlescaper, attrescaper и htmlescaper — псевдонимы для внутренних функций экранирования.
Для этих внутренних функций экранирования, если конвейер действий оценивает значение интерфейса nil, он обрабатывается так, как будто это пустая строка.
Namespaced и data-атрибуты
Атрибуты с пространством имен обрабатываются так, как будто у них нет пространства имен. Рассмотрим пример:
Во время анализа атрибут будет обработан так, как если бы это был просто “href”. Таким образом, во время анализа шаблон становится:
<a my:href="{{. | urlescaper | attrescaper}}"></a>
Аналогично атрибутам с пространствами имен, атрибуты с префиксом “data-” обрабатываются так, как если бы у них не было префикса “data-”. Рассмотрим пример:
<a data-href="{{.}}"></a>
Во время анализа это становится
<a data-href="{{. | urlescaper | attrescaper}}"></a>
Если атрибут имеет как пространство имен, так и префикс “data-”, то при определении контекста будет удалено только пространство имен. Например:
<a my:data-href="{{.}}"></a>
Это обрабатывается так, как если бы “my:data-href” было просто “data-href”, а не “href”, как было бы, если бы префикс “data-” тоже игнорировался. Таким образом, во время анализа это становится просто
<a my:data-href="{{. | attrescaper}}"></a>
В качестве особого случая атрибуты с пространством имен “xmlns” всегда рассматриваются как содержащие URL-адреса. Например:
<a xmlns:title="{{.}}"></a>
<a xmlns:href="{{.}}"></a>
<a xmlns:onclick="{{.}}"></a>
Во время анализа они становятся:
<a xmlns:title="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:href="{{. | urlescaper | attrescaper}}"></a>
<a xmlns:onclick="{{. | urlescaper | attrescaper}}"></a>
Ошибки
Подробности см. в документации ErrorCode.
Более полная картина
Остальную часть этого комментария к пакету можно пропустить при первом чтении; он включает детали, необходимые для понимания экранирования контекстов и сообщений об ошибках. Большинству пользователей не нужно понимать эти детали.
Контексты
Предположив, что {{.}} — это O'Reilly: How are <i>you</i>?
, в таблице ниже показано, как выглядит {{.}} при использовании в контексте слева.
Context {{.}} After
{{.}} O'Reilly: How are <i>you</i>?
<a title='{{.}}'> O'Reilly: How are you?
<a href="/{{.}}"> O'Reilly: How are %3ci%3eyou%3c/i%3e?
<a href="?q={{.}}"> O'Reilly%3a%20How%20are%3ci%3e...%3f
<a onx='f("{{.}}")'> O\x27Reilly: How are \x3ci\x3eyou...?
<a onx='f({{.}})'> "O\x27Reilly: How are \x3ci\x3eyou...?"
<a onx='pattern = /{{.}}/;'> O\x27Reilly: How are \x3ci\x3eyou...\x3f
При использовании в небезопасном контексте значение может быть отфильтровано:
Context {{.}} After
<a href="{{.}}"> #ZgotmplZ
поскольку “O’Reilly:” не является разрешенным протоколом, как “http:”.
Если {{.}} — это безобидное слово left
, то оно может появляться более широко,
Context {{.}} After
{{.}} left
<a title='{{.}}'> left
<a href='{{.}}'> left
<a href='/{{.}}'> left
<a href='?dir={{.}}'> left
<a style="border-{{.}}: 4px"> left
<a style="align: {{.}}"> left
<a style="background: '{{.}}'> left
<a style="background: url('{{.}}')> left
<style>p.{{.}} {color:red}</style> left
Нестроковые значения могут использоваться в контекстах JavaScript. Если {{.}} — это
struct{A,B string}{ "foo", "bar" }
в экранированном шаблоне
<script>var pair = {{.}};</script>
то вывод шаблона будет
<script>var pair = {"A": "foo", "B": "bar"};</script>
Ознакомьтесь с пакетом json, чтобы понять, как нестроковый контент маршалируется для встраивания в контексты JavaScript.
Типизированные строки
По умолчанию этот пакет предполагает, что все конвейеры производят простую текстовую строку. Он добавляет этапы экранирования конвейера, необходимые для корректного и безопасного встраивания этой простой текстовой строки в соответствующий контекст.
Если значение данных не является обычным текстом, вы можете убедиться, что оно не экранировано слишком сильно, пометив его разными типами.
Типы HTML, JS, URL и другие из content.go могут содержать безопасный контент, который освобожден от экранирования.
Например шаблон:
может быть вызван с помощью
tmpl.Execute(out, template.HTML(`<b>World</b>`))
вывод
вместо
Hello, <b>World<b>!
который был бы получен, если бы {{.}} был обычной строкой.
Модель безопасности
Определение “безопасности”, используемое в этом пакете, основано на спецификации.
Данный пакет исходит из следующих допущений:
- Авторы шаблонов считаются доверенными
- Данные, передаваемые в
Execute
, — ненадёжные
При этом обеспечиваются следующие свойства безопасности при работе с ненадёжными данными:
1. Сохранение структуры (Structure Preservation Property)
Если автор шаблона пишет HTML-тег на безопасном языке шаблонов, браузер всегда будет интерпретировать соответствующую часть вывода именно как тег — независимо от значений ненадёжных данных. То же самое относится к другим структурам, таким как границы атрибутов, строки JavaScript и CSS.
2. Контроль исполнения кода (Code Effect Property)
В результате вставки вывода шаблона на страницу должен выполняться только код, явно указанный автором шаблона. При этом весь код, указанный автором, должен выполняться корректно.
3. Принцип наименьшего удивления (Least Surprise Property)
Разработчик (или ревьюер кода), знакомый с HTML, CSS и JavaScript и знающий о контекстном автоэкранировании, должен быть способен, увидев {{.}}
, однозначно определить, какое экранирование будет применено.
Поддержка шаблонных литералов ES6
Ранее шаблонные литералы ECMAScript 6 (${...}
) были отключены по умолчанию, и их можно было включить через переменную окружения GODEBUG=jstmpllitinterp=1
.
Сейчас шаблонные литералы поддерживаются по умолчанию, а настройка jstmpllitinterp
больше не имеет эффекта.
Пример
package main
import (
"html/template"
"log"
"os"
)
func main() {
const tpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
{{range .Items}}<div>{{ . }}</div>{{else}}<div><strong>no rows</strong></div>{{end}}
</body>
</html>`
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
t, err := template.New("webpage").Parse(tpl)
check(err)
data := struct {
Title string
Items []string
}{
Title: "My page",
Items: []string{
"My photos",
"My blog",
},
}
err = t.Execute(os.Stdout, data)
check(err)
noItems := struct {
Title string
Items []string
}{
Title: "My another page",
Items: []string{},
}
err = t.Execute(os.Stdout, noItems)
check(err)
}
Output:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My page</title>
</head>
<body>
<div>My photos</div><div>My blog</div>
</body>
</html>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My another page</title>
</head>
<body>
<div><strong>no rows</strong></div>
</body>
</html>
Пример Autoescaping
package main
import (
"html/template"
"log"
"os"
)
func main() {
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
t, err := template.New("foo").Parse(`{{define "T"}}Hello, {{.}}!{{end}}`)
check(err)
err = t.ExecuteTemplate(os.Stdout, "T", "<script>alert('you have been pwned')</script>")
check(err)
}
Output:
Hello, <script>alert('you have been pwned')</script>!
Пример Escape
package main
import (
"fmt"
"html/template"
"os"
)
func main() {
const s = `"Fran & Freddie's Diner" <tasty@example.com>`
v := []any{`"Fran & Freddie's Diner"`, ' ', `<tasty@example.com>`}
fmt.Println(template.HTMLEscapeString(s))
template.HTMLEscape(os.Stdout, []byte(s))
fmt.Fprintln(os.Stdout, "")
fmt.Println(template.HTMLEscaper(v...))
fmt.Println(template.JSEscapeString(s))
template.JSEscape(os.Stdout, []byte(s))
fmt.Fprintln(os.Stdout, "")
fmt.Println(template.JSEscaper(v...))
fmt.Println(template.URLQueryEscaper(v...))
}
Output:
"Fran & Freddie's Diner" <tasty@example.com>
"Fran & Freddie's Diner" <tasty@example.com>
"Fran & Freddie's Diner"32<tasty@example.com>
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\" \u003Ctasty@example.com\u003E
\"Fran \u0026 Freddie\'s Diner\"32\u003Ctasty@example.com\u003E
%22Fran+%26+Freddie%27s+Diner%2232%3Ctasty%40example.com%3E
1 - Шпаргалка по html/template в Go
html/template - это пакет Go для безопасного создания HTML-выхода с автоматическим экранированием.
Инициализация шаблона
import "html/template"
// Создание нового шаблона
tmpl := template.New("templateName")
// Парсинг шаблона из строки
tmpl, err := template.New("test").Parse(`<h1>{{.Title}}</h1>`)
// Парсинг шаблона из файла
tmpl, err := template.ParseFiles("template.html")
// Парсинг нескольких файлов
tmpl, err := template.ParseGlob("templates/*.html")
Основные конструкции в шаблоне
1. Вывод значения
<!-- Простой вывод -->
<p>{{.UserName}}</p>
<!-- Вывод с экранированием HTML -->
<p>{{.UserComment}}</p>
<!-- Вывод без экранирования (осторожно!) -->
<p>{{.SafeHTML | html}}</p>
2. Условия
{{if .User}}
<p>Welcome, {{.User}}!</p>
{{else}}
<p>Please log in.</p>
{{end}}
{{if eq .Role "admin"}}
<p>Admin dashboard</p>
{{end}}
3. Циклы
<ul>
{{range .Items}}
<li>{{.}}</li>
{{end}}
</ul>
<!-- С индексом -->
<ul>
{{range $index, $item := .Items}}
<li>Item #{{$index}}: {{$item}}</li>
{{end}}
</ul>
<!-- Если слайс пуст -->
{{range .Items}}
{{.}}
{{else}}
<p>No items found</p>
{{end}}
4. Определение переменных
{{$name := .UserName}}
{{$count := len .Items}}
<p>Hello, {{$name}}. You have {{$count}} items.</p>
5. Функции и конвейеры
<!-- Вызов встроенных функций -->
<p>{{printf "Hello, %s" .Name}}</p>
<p>{{len .Items}} items</p>
<p>{{index .Items 0}}</p>
<!-- Конвейер -->
<p>{{.Title | upper | truncate 50}}</p>
6. Вложенные шаблоны
// Определение в коде
tmpl := template.Must(template.New("base").Parse(`
{{define "content"}}Default content{{end}}
<html>
<body>
{{template "content" .}}
</body>
</html>
`))
// Переопределение в другом шаблоне
tmpl2 := template.Must(tmpl.Parse(`{{define "content"}}New content{{end}}`))
<!-- В шаблоне -->
{{template "header" .}}
<!-- С передачей других данных -->
{{template "footer" .FooterData}}
7. Пользовательские функции
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"add": func(a, b int) int { return a + b },
}
tmpl := template.New("test").Funcs(funcMap)
tmpl, err := tmpl.Parse(`{{upper .Name}} {{add 1 2}}`)
Пример полного шаблона
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}} - My Site</title>
</head>
<body>
<header>
{{if .User}}
<p>Welcome, {{.User.Name}} ({{.User.Email}})</p>
{{else}}
<a href="/login">Login</a>
{{end}}
</header>
<nav>
<ul>
{{range .Menu}}
<li><a href="{{.URL}}">{{.Title}}</a></li>
{{end}}
</ul>
</nav>
<main>
{{template "content" .}}
</main>
<footer>
<p>© {{.CurrentYear}} My Company</p>
</footer>
</body>
</html>
Выполнение шаблона
// В память
err := tmpl.Execute(os.Stdout, data)
// В строку
var buf bytes.Buffer
err := tmpl.Execute(&buf, data)
result := buf.String()
// Конкретный шаблон из набора
err := tmpl.ExecuteTemplate(os.Stdout, "templateName", data)
Обработка ошибок
tmpl := template.Must(template.New("").Parse("...")) // паника при ошибке
if err := tmpl.Execute(w, data); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
Безопасность
- Автоматическое экранирование HTML/JS/CSS
- Для отключения экранирования используйте типы
template.HTML
, template.JS
, template.CSS
:
type Data struct {
SafeContent template.HTML
}
data := Data{SafeContent: template.HTML("<b>Safe HTML</b>")}
2 - Подробное руководство по define, template и block в html/template
В пакете html/template есть три основные директивы для работы с повторно используемыми частями шаблонов: define, template, block
Основные концепции
В пакете html/template
есть три основные директивы для работы с повторно используемыми частями шаблонов:
{{define "name"}}...{{end}}
- определяет именованный шаблон
{{template "name"}}
- вставляет именованный шаблон
{{block "name"}}...{{end}}
- комбинация define+template с возможностью переопределения
1. Директива define
Где описывать:
- В любом месте шаблона (но обычно в начале или конце файла)
- В отдельных файлах, которые потом объединяются
Пример определения:
{{define "header"}}
<header>
<h1>{{.Title}}</h1>
<nav>
<a href="/">Home</a>
<a href="/about">About</a>
</nav>
</header>
{{end}}
{{define "footer"}}
<footer>
<p>Copyright © {{.Year}} My Company</p>
</footer>
{{end}}
2. Директива template
Где применять:
- В любом месте основного шаблона
- Можно передавать данные (по умолчанию - текущий контекст)
Пример использования:
<!DOCTYPE html>
<html>
<head>
<title>{{.Title}}</title>
</head>
<body>
{{template "header" .}} <!-- Передаем текущий контекст -->
<main>
{{.Content}}
</main>
{{template "footer" .}} <!-- Передаем текущий контекст -->
</body>
</html>
Передача другого контекста:
{{template "user_profile" .UserData}}
3. Директива block
Особенности:
- Комбинация
define
+ template
- Можно переопределять в дочерних шаблонах
- Если не переопределен - используется содержимое по умолчанию
Пример:
<!-- base.html -->
<!DOCTYPE html>
<html>
<head>
{{block "head" .}}
<title>{{.Title}} - Default</title>
{{end}}
</head>
<body>
{{block "content" .}}
<p>Default content</p>
{{end}}
</body>
</html>
Переопределение в дочернем шаблоне:
<!-- home.html -->
{{define "content"}}
<h1>Welcome!</h1>
<p>This is the home page.</p>
{{end}}
{{template "base.html" .}}
Практические схемы организации
1. Многофайловая структура
templates/
├── base.html # Основной каркас
├── header.html # Шапка сайта
├── footer.html # Подвал
├── home.html # Главная страница
└── about.html # Страница "О нас"
base.html:
<!DOCTYPE html>
<html>
<head>
{{template "head" .}}
</head>
<body>
{{template "header" .}}
{{block "content" .}}{{end}}
{{template "footer" .}}
</body>
</html>
home.html:
{{define "content"}}
<h1>Welcome</h1>
<p>Home page content</p>
{{end}}
{{template "base.html" .}}
2. Наследование шаблонов
// Загрузка шаблонов
templates := template.Must(template.ParseFiles(
"base.html",
"header.html",
"footer.html",
"home.html",
))
// Исполнение конкретного шаблона
err := templates.ExecuteTemplate(w, "home.html", data)
3. Вложенные блоки
<!-- base.html -->
{{define "layout"}}
<div class="container">
{{block "sidebar" .}}
<!-- Default sidebar -->
<div class="sidebar">Default sidebar</div>
{{end}}
<div class="content">
{{template "content" .}}
</div>
</div>
{{end}}
<!-- profile.html -->
{{define "sidebar"}}
<div class="profile-sidebar">
<img src="{{.User.Avatar}}">
<h3>{{.User.Name}}</h3>
</div>
{{end}}
{{define "content"}}
<div class="profile-content">
<!-- Контент профиля -->
</div>
{{end}}
{{template "layout" .}}
Особенности работы
- Порядок загрузки: шаблоны, на которые есть ссылки, должны быть загружены до их использования
- Контекст данных: по умолчанию передается текущий контекст, но можно передать другой
- Рекурсия: шаблоны могут вызывать сами себя (но осторожно с бесконечной рекурсией)
- Переопределение: последний загруженный шаблон с тем же именем переопределяет предыдущий
Пример с пользовательскими функциями
func main() {
funcMap := template.FuncMap{
"formatDate": func(t time.Time) string {
return t.Format("2006-01-02")
},
}
tmpl := template.Must(
template.New("base").Funcs(funcMap).ParseFiles("base.html", "home.html"),
)
err := tmpl.ExecuteTemplate(os.Stdout, "home.html", data)
}
<!-- В шаблоне: -->
<p>Created: {{.CreatedAt | formatDate}}</p>
Лучшие практики
- Разделяйте шаблоны на логические компоненты
- Используйте
block
для переопределяемых секций
- Основной каркас выносите в
base.html
- Называйте шаблоны осмысленно (не “template1”, а “user_profile”)
- Документируйте назначение шаблонов в комментариях
3 - Шпаргалка по конвейерам и функциям в html/template (Go)
Конвейеры позволяют последовательно применять функции к данным в шаблонах.
Синтаксис:
{{value | function1 | function2 | ...}}
Примеры:
<!-- Простой конвейер -->
<p>{{.Name | upper}}</p>
<!-- Множественные функции -->
<p>{{.Title | trim | lower | trunc 50}}</p>
<!-- С аргументами -->
<p>{{.Date | format "2006-01-02"}}</p>
<!-- Вложенные конвейеры -->
<p>{{.Description | trunc 100 | html}}</p>
Встроенные функции
1. Строковые функции
{{"hello" | title}} <!-- Hello -->
{{"HELLO" | lower}} <!-- hello -->
{{"hello" | upper}} <!-- HELLO -->
{{" hello " | trim}} <!-- hello -->
{{"hello" | repeat 3}} <!-- hellohellohello -->
2. HTML-функции
{{"<script>" | html}} <!-- Экранирование HTML -->
{{"<b>safe</b>" | html}} <!-- <b>safe</b> -->
{{"<b>safe</b>" | safeHTML}} <!-- <b>safe</b> (без экранирования) -->
3. Функции для коллекций
{{len .Items}} <!-- Длина массива/слайса/мапы -->
{{index .Users 0}} <!-- Первый элемент -->
{{index .Map "key"}} <!-- Значение по ключу -->
{{slice .List 1 3}} <!-- Подслайс -->
4. Логические функции
{{if eq .A .B}}...{{end}} <!-- Равно -->
{{if ne .A .B}}...{{end}} <!-- Не равно -->
{{if lt .A .B}}...{{end}} <!-- Меньше -->
{{if le .A .B}}...{{end}} <!-- Меньше или равно -->
{{if gt .A .B}}...{{end}} <!-- Больше -->
{{if ge .A .B}}...{{end}} <!-- Больше или равно -->
{{if and .A .B}}...{{end}} <!-- Логическое И -->
{{if or .A .B}}...{{end}} <!-- Логическое ИЛИ -->
{{if not .A}}...{{end}} <!-- Логическое НЕ -->
5. Математические функции
{{add 1 2}} <!-- 3 -->
{{sub 5 2}} <!-- 3 -->
{{mul 3 4}} <!-- 12 -->
{{div 10 2}} <!-- 5 -->
{{mod 10 3}} <!-- 1 -->
6. Функции форматирования
{{printf "%s - %d" .Name .Age}} <!-- Форматированная строка -->
{{.Price | printf "$%.2f"}} <!-- Форматирование числа -->
{{.Date | date "2006-01-02"}} <!-- Форматирование даты -->
Пользовательские функции
Регистрация функций:
funcMap := template.FuncMap{
"upper": strings.ToUpper,
"trunc": func(s string, max int) string {
if len(s) > max {
return s[:max] + "..."
}
return s
},
"add": func(a, b int) int { return a + b },
"contains": strings.Contains,
"now": time.Now,
}
tmpl := template.New("").Funcs(funcMap)
Использование в шаблоне:
{{.Title | trunc 50 | upper}}
{{add .Value 10}}
{{if contains .Description "important"}}Important!{{end}}
{{now | date "2006-01-02"}}
Примеры сложных конвейеров
1. Обработка текста
{{.UserComment | trim | trunc 200 | html}}
2. Форматирование данных
{{.CreatedAt | date "02.01.2006" | printf "Created: %s"}}
3. Работа с коллекциями
{{range .Items | sortBy "Name" | limit 10}}
{{.Name | upper}}
{{end}}
4. Условные конвейеры
{{if .Price | gt 100}}
<p class="expensive">{{.Price | printf "$%.2f"}}</p>
{{else}}
<p>{{.Price | printf "$%.2f"}}</p>
{{end}}
Полезные комбинации
Безопасный вывод HTML:
{{.UserContent | sanitizeHTML | safeHTML}}
Форматирование денежных значений:
{{.Amount | div 100 | printf "$%.2f"}}
Сортировка и фильтрация:
{{range .Products | filter "category" "electronics" | sortBy "price"}}
{{.Name}} - {{.Price | printf "$%.2f"}}
{{end}}
Ограничения
- Функции должны возвращать только одно значение (и опционально ошибку)
- Невозможно передавать функции как аргументы
- Нет поддержки сложных операций с каналами
- Автоматическое экранирование применяется к конечному результату
Лучшие практики
- Выносите сложную логику в Go-код, а не в шаблоны
- Используйте конвейеры для читаемости, но не переусердствуйте
- Документируйте пользовательские функции
- Тестируйте шаблоны с разными входными данными
- Избегайте побочных эффектов в функциях
4 - Функции экранирования HTML/JavaScript/URL пакета html/template языка Go
Описание функций пакета html/template языка Go в соответствии с официальной документацией
HTMLEscape
func HTMLEscape(w io.Writer, b []byte)
HTMLEscape записывает в w
экранированный HTML-эквивалент текстовых данных b
.
HTMLEscapeString
func HTMLEscapeString(s string) string
HTMLEscapeString возвращает экранированный HTML-эквивалент текстовых данных s
.
HTMLEscaper
func HTMLEscaper(args ...any) string
HTMLEscaper возвращает экранированный HTML-эквивалент текстового представления своих аргументов.
IsTrue
func IsTrue(val any) (truth, ok bool)
IsTrue определяет, является ли значение “истинным” (не нулевым для своего типа) и имеет ли оно вообще смысл как булево значение. Это определение истинности используется в условных операциях типа if
.
JSEscape
func JSEscape(w io.Writer, b []byte)
JSEscape записывает в w
экранированный JavaScript-эквивалент текстовых данных b
.
JSEscapeString
func JSEscapeString(s string) string
JSEscapeString возвращает экранированный JavaScript-эквивалент текстовых данных s
.
JSEscaper
func JSEscaper(args ...any) string
JSEscaper возвращает экранированный JavaScript-эквивалент текстового представления своих аргументов.
URLQueryEscaper
func URLQueryEscaper(args ...any) string
URLQueryEscaper возвращает экранированное значение текстового представления аргументов в форме, пригодной для вставки в URL-запрос.
5 - Типы экранирования HTML/JavaScript/URL пакета html/template языка Go
Описание типов и функций пакета html/template языка Go в соответствии с официальной документацией
type CSS
CSS инкапсулирует заранее проверенное содержимое, соответствующее:
- Производству CSS3 таблиц стилей (напр.
p { color: purple }
)
- Производству CSS3 правил (напр.
a[href=~"https:"].foo#bar
)
- Производству CSS3 деклараций (напр.
color: red; margin: 2px
)
- Производству CSS3 значений (напр.
rgba(0, 0, 255, 127)
)
Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.
type Error
type Error struct {
ErrorCode ErrorCode // Тип ошибки
Node parse.Node // Узел, вызвавший проблему (если известен)
Name string // Имя шаблона с ошибкой
Line int // Номер строки в исходнике шаблона
Description string // Человекочитаемое описание
}
Описывает проблему, возникшую при экранировании шаблона.
Коды ошибок (ErrorCode)
const (
OK ErrorCode = iota // Нет ошибки
// Неоднозначный контекст в URL
ErrAmbigContext
// Некорректный HTML (незакрытые теги/атрибуты)
ErrBadHTML
// Ветки if/range заканчиваются в разных контекстах
ErrBranchEnd
// Шаблон заканчивается в нетекстовом контексте
ErrEndContext
// Отсутствует указанный шаблон
ErrNoSuchTemplate
// Невозможно вычислить контекст вывода
ErrOutputContext
// Незавершённый набор символов в regexp JS
ErrPartialCharset
// Незавершённая escape-последовательность
ErrPartialEscape
// Повторный вход в range с изменением контекста
ErrRangeLoopReentry
// Неоднозначная интерпретация '/' (деление или regexp)
ErrSlashAmbig
// Использование запрещённых экранировщиков
ErrPredefinedEscaper
// Устаревшая ошибка для JS template literals
ErrJSTemplate
)
Особое значение ZgotmplZ
Пример:
где {{.X}}
вычисляется в javascript:...
Результат:
Объяснение:
“ZgotmplZ” - специальное значение, указывающее, что небезопасное содержимое достигло CSS или URL контекста во время выполнения. Для доверенных источников используйте типы содержимого для отключения фильтрации: URL(
javascript:…)
.
type FuncMap
type FuncMap = template.FuncMap
FuncMap - это псевдоним для типа template.FuncMap
из пакета text/template, представляющий карту функций для использования в шаблонах.
type HTML
HTML инкапсулирует безопасный фрагмент HTML-документа. Не следует использовать для:
- HTML из ненадёжных источников
- HTML с незакрытыми тегами или комментариями
Внимание: Содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.
type HTMLAttr
HTMLAttr инкапсулирует безопасный HTML-атрибут, например dir="ltr"
.
Внимание: Как и с HTML, содержимое должно быть доверенным, так как включается без экранирования.
type JS
JS инкапсулирует безопасное выражение EcmaScript5, например (x + y * z())
.
Особенности:
- Авторы шаблонов должны обеспечивать правильный порядок операций
- Не должно быть неоднозначности между выражениями и инструкциями
Внимание: Не безопасно использовать для непроверенного JSON. Для JSON лучше использовать json.Unmarshal
.
type JSStr
JSStr инкапсулирует безопасную строку для JavaScript, которая будет заключена в кавычки.
Требования:
- Допустимые символы или escape-последовательности
- Запрещены переносы строк внутри escape-последовательностей
Пример:
JSStr("foo\\nbar") // допустимо
JSStr("foo\\\nbar") // недопустимо
type Srcset
Srcset инкапсулирует безопасный атрибут srcset для тегов <img>
(спецификация HTML5).
type Template
type Template struct {
Tree *parse.Tree // Абстрактное синтаксическое дерево шаблона
// ... скрытые поля
}
Template - это специализированная версия text/template
, которая генерирует безопасные фрагменты HTML-документов.
Ключевые особенности:
- Расширяет базовый функционал text/template
- Автоматически применяет контекстно-зависимое экранирование
- Гарантирует безопасность вывода при правильном использовании
Все типы безопасного контента (HTML, JS и др.) требуют, чтобы их содержимое поступало из доверенных источников, так как они включаются в вывод без дополнительного экранирования.
Пример Block
package main
import (
"html/template"
"log"
"os"
"strings"
)
func main() {
const (
master = `Names:{{block "list" .}}{{"\n"}}{{range .}}{{println "-" .}}{{end}}{{end}}`
overlay = `{{define "list"}} {{join . ", "}}{{end}} `
)
var (
funcs = template.FuncMap{"join": strings.Join}
guardians = []string{"Gamora", "Groot", "Nebula", "Rocket", "Star-Lord"}
)
masterTmpl, err := template.New("master").Funcs(funcs).Parse(master)
if err != nil {
log.Fatal(err)
}
overlayTmpl, err := template.Must(masterTmpl.Clone()).Parse(overlay)
if err != nil {
log.Fatal(err)
}
if err := masterTmpl.Execute(os.Stdout, guardians); err != nil {
log.Fatal(err)
}
if err := overlayTmpl.Execute(os.Stdout, guardians); err != nil {
log.Fatal(err)
}
}
Output:
Names:
- Gamora
- Groot
- Nebula
- Rocket
- Star-Lord
Names: Gamora, Groot, Nebula, Rocket, Star-Lord
Пример Glob
Здесь мы демонстрируем загрузку набора шаблонов из каталога.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T0.tmpl is a plain template file that just invokes T1.
{"T0.tmpl", `T0 invokes T1: ({{template "T1"}})`},
// T1.tmpl defines a template, T1 that invokes T2.
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// T0.tmpl is the first name matched, so it becomes the starting template,
// the value returned by ParseGlob.
tmpl := template.Must(template.ParseGlob(pattern))
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
log.Fatalf("template execution: %s", err)
}
}
Output:
T0 invokes T1: (T1 invokes T2: (This is T2))
Пример Helpers
Этот пример демонстрирует один из способов поделиться некоторыми шаблонами и использовать их в разных контекстах. В этом варианте мы вручную добавляем несколько шаблонов драйверов в существующий пакет шаблонов.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T1.tmpl defines a template, T1 that invokes T2.
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// Load the helpers.
templates := template.Must(template.ParseGlob(pattern))
// Add one driver template to the bunch; we do this with an explicit template definition.
_, err := templates.Parse("{{define `driver1`}}Driver 1 calls T1: ({{template `T1`}})\n{{end}}")
if err != nil {
log.Fatal("parsing driver1: ", err)
}
// Add another driver template.
_, err = templates.Parse("{{define `driver2`}}Driver 2 calls T2: ({{template `T2`}})\n{{end}}")
if err != nil {
log.Fatal("parsing driver2: ", err)
}
// We load all the templates before execution. This package does not require
// that behavior but html/template's escaping does, so it's a good habit.
err = templates.ExecuteTemplate(os.Stdout, "driver1", nil)
if err != nil {
log.Fatalf("driver1 execution: %s", err)
}
err = templates.ExecuteTemplate(os.Stdout, "driver2", nil)
if err != nil {
log.Fatalf("driver2 execution: %s", err)
}
}
Output:
Driver 1 calls T1: (T1 invokes T2: (This is T2))
Driver 2 calls T2: (This is T2)
Пример Parsefiles
Здесь мы демонстрируем загрузку набора шаблонов из файлов в разных каталогах.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create different temporary directories and populate them with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir1 := createTestDir([]templateFile{
// T1.tmpl is a plain template file that just invokes T2.
{"T1.tmpl", `T1 invokes T2: ({{template "T2"}})`},
})
dir2 := createTestDir([]templateFile{
// T2.tmpl defines a template T2.
{"T2.tmpl", `{{define "T2"}}This is T2{{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer func(dirs ...string) {
for _, dir := range dirs {
os.RemoveAll(dir)
}
}(dir1, dir2)
// Here starts the example proper.
// Let's just parse only dir1/T0 and dir2/T2
paths := []string{
filepath.Join(dir1, "T1.tmpl"),
filepath.Join(dir2, "T2.tmpl"),
}
tmpl := template.Must(template.ParseFiles(paths...))
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
log.Fatalf("template execution: %s", err)
}
}
Output:
T1 invokes T2: (This is T2)
Пример Share
В этом примере показано, как использовать одну группу шаблонов драйверов с различными наборами вспомогательных шаблонов.
package main
import (
"io"
"log"
"os"
"path/filepath"
"text/template"
)
// templateFile defines the contents of a template to be stored in a file, for testing.
type templateFile struct {
name string
contents string
}
func createTestDir(files []templateFile) string {
dir, err := os.MkdirTemp("", "template")
if err != nil {
log.Fatal(err)
}
for _, file := range files {
f, err := os.Create(filepath.Join(dir, file.name))
if err != nil {
log.Fatal(err)
}
defer f.Close()
_, err = io.WriteString(f, file.contents)
if err != nil {
log.Fatal(err)
}
}
return dir
}
func main() {
// Here we create a temporary directory and populate it with our sample
// template definition files; usually the template files would already
// exist in some location known to the program.
dir := createTestDir([]templateFile{
// T0.tmpl is a plain template file that just invokes T1.
{"T0.tmpl", "T0 ({{.}} version) invokes T1: ({{template `T1`}})\n"},
// T1.tmpl defines a template, T1 that invokes T2. Note T2 is not defined
{"T1.tmpl", `{{define "T1"}}T1 invokes T2: ({{template "T2"}}){{end}}`},
})
// Clean up after the test; another quirk of running as an example.
defer os.RemoveAll(dir)
// pattern is the glob pattern used to find all the template files.
pattern := filepath.Join(dir, "*.tmpl")
// Here starts the example proper.
// Load the drivers.
drivers := template.Must(template.ParseGlob(pattern))
// We must define an implementation of the T2 template. First we clone
// the drivers, then add a definition of T2 to the template name space.
// 1. Clone the helper set to create a new name space from which to run them.
first, err := drivers.Clone()
if err != nil {
log.Fatal("cloning helpers: ", err)
}
// 2. Define T2, version A, and parse it.
_, err = first.Parse("{{define `T2`}}T2, version A{{end}}")
if err != nil {
log.Fatal("parsing T2: ", err)
}
// Now repeat the whole thing, using a different version of T2.
// 1. Clone the drivers.
second, err := drivers.Clone()
if err != nil {
log.Fatal("cloning drivers: ", err)
}
// 2. Define T2, version B, and parse it.
_, err = second.Parse("{{define `T2`}}T2, version B{{end}}")
if err != nil {
log.Fatal("parsing T2: ", err)
}
// Execute the templates in the reverse order to verify the
// first is unaffected by the second.
err = second.ExecuteTemplate(os.Stdout, "T0.tmpl", "second")
if err != nil {
log.Fatalf("second execution: %s", err)
}
err = first.ExecuteTemplate(os.Stdout, "T0.tmpl", "first")
if err != nil {
log.Fatalf("first: execution: %s", err)
}
}
Output:
T0 (second version) invokes T1: (T1 invokes T2: (T2, version B))
T0 (first version) invokes T1: (T1 invokes T2: (T2, version A))
func Must
func Must(t *Template, err error) *Template
Must - вспомогательная функция, которая оборачивает вызов функции, возвращающей (*Template, error), и вызывает панику, если ошибка не равна nil. Предназначена для инициализации переменных:
Пример:
var t = template.Must(template.New("name").Parse("html"))
func New
func New(name string) *Template
New создает новый HTML-шаблон с указанным именем.
func ParseFS (добавлена в Go 1.16)
func ParseFS(fs fs.FS, patterns ...string) (*Template, error)
ParseFS аналогична ParseFiles и ParseGlob, но читает из файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).
func ParseFiles
func ParseFiles(filenames ...string) (*Template, error)
ParseFiles создает новый шаблон и парсит определения шаблонов из указанных файлов. Имя возвращаемого шаблона будет соответствовать базовому имени первого файла. Должен быть указан хотя бы один файл.
Особенности:
- При парсинге нескольких файлов с одинаковыми именами будет использован последний указанный файл
- Например,
ParseFiles("a/foo", "b/foo")
сохранит “b/foo” как шаблон “foo”, а “a/foo” будет недоступен
func ParseGlob
func ParseGlob(pattern string) (*Template, error)
ParseGlob создает новый шаблон и парсит определения шаблонов из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match). Шаблон должен соответствовать хотя бы одному файлу.
Эквивалентно вызову ParseFiles со списком файлов, соответствующих шаблону. Как и в ParseFiles, при дублировании имен используется последний файл.
func (*Template) AddParseTree
func (t *Template) AddParseTree(name string, tree *parse.Tree) (*Template, error)
AddParseTree создает новый шаблон с указанным именем и деревом разбора, ассоциируя его с t.
Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены.
func (*Template) Clone
func (t *Template) Clone() (*Template, error)
Clone создает копию шаблона, включая все ассоциированные шаблоны. Последующие вызовы Parse в копии будут добавлять шаблоны только в копию.
Возвращает ошибку, если t уже был выполнен.
func (*Template) DefinedTemplates (добавлена в Go 1.6)
func (t *Template) DefinedTemplates() string
DefinedTemplates возвращает строку с перечислением определенных шаблонов с префиксом “; defined templates are: “. Если шаблонов нет, возвращает пустую строку. Используется для генерации сообщений об ошибках.
func (*Template) Delims
func (t *Template) Delims(left, right string) *Template
Delims устанавливает разделители действий для последующих вызовов Parse, ParseFiles или ParseGlob. Вложенные определения шаблонов наследуют эти настройки. Пустые разделители соответствуют значениям по умолчанию: {{
и }}
.
Возвращает сам шаблон, позволяя объединять вызовы в цепочку.
Пример
package main
import (
"html/template"
"log"
"os"
)
func main() {
const text = "<<.Greeting>> {{.Name}}"
data := struct {
Greeting string
Name string
}{
Greeting: "Hello",
Name: "Joe",
}
t := template.Must(template.New("tpl").Delims("<<", ">>").Parse(text))
err := t.Execute(os.Stdout, data)
if err != nil {
log.Fatal(err)
}
}
func (*Template) Execute
func (t *Template) Execute(wr io.Writer, data any) error
Применяет разобранный шаблон к указанным данным, записывая результат в wr
. В случае ошибки выполнение прерывается, но частичные результаты могут быть уже записаны. Шаблон можно выполнять параллельно, но при использовании общего Writer вывод может перемешиваться.
func (*Template) ExecuteTemplate
func (t *Template) ExecuteTemplate(wr io.Writer, name string, data any) error
Применяет шаблон с указанным именем, ассоциированный с t
, к данным и записывает результат в wr
. Поведение при ошибках и параллельном выполнении аналогично Execute
.
Отличие Execute от ExecuteTemplate
Отличие между Execute
и ExecuteTemplate
Основное различие между этими методами заключается в том, какой шаблон они выполняют:
Execute
- выполняет основной (корневой) шаблон
ExecuteTemplate
- выполняет указанный именованный шаблон (включая ассоциированные шаблоны)
Пример использования:
package main
import (
"os"
"text/template"
)
func main() {
// Создаем шаблон с именем "base"
tmpl := template.Must(template.New("base").Parse(`
{{define "header"}}<h1>Заголовок</h1>{{end}}
{{define "content"}}<p>Основное содержимое</p>{{end}}
<!DOCTYPE html>
<html>
<head><title>Пример</title></head>
<body>
{{template "header"}}
{{template "content"}}
</body>
</html>
`))
// 1. Execute - выполнит основной шаблон "base"
err := tmpl.Execute(os.Stdout, nil)
if err != nil {
panic(err)
}
// 2. ExecuteTemplate - можно выполнить любой из определенных шаблонов
err = tmpl.ExecuteTemplate(os.Stdout, "header", nil)
if err != nil {
panic(err)
}
}
Ключевые отличия:
Характеристика |
Execute |
ExecuteTemplate |
Какой шаблон выполняется |
Только основной (корневой) |
Любой именованный (включая основной) |
Использование |
Когда нужно выполнить весь шаблон |
Когда нужна часть шаблона |
Производительность |
Чуть быстрее |
Чуть медленнее (поиск по имени) |
Гибкость |
Меньше |
Больше (доступ ко всем шаблонам) |
Когда использовать:
Execute
- для рендеринга всего шаблона целиком
ExecuteTemplate
- когда нужно:
- выполнить часть шаблона
- переиспользовать компоненты в разных контекстах
- отладить отдельный блок шаблона
func (*Template) Funcs
func (t *Template) Funcs(funcMap FuncMap) *Template
Добавляет функции из переданной мапы в функциональную мапу шаблона. Должен вызываться до парсинга шаблона. Вызывает панику, если значение в мапе не является функцией с подходящим типом возвращаемого значения. Возвращает сам шаблон для цепочки вызовов.
func (*Template) Lookup
func (t *Template) Lookup(name string) *Template
Возвращает шаблон с указанным именем, ассоциированный с t
, или nil
если такого шаблона нет.
func (*Template) Name
func (t *Template) Name() string
Возвращает имя шаблона.
func (*Template) New
func (t *Template) New(name string) *Template
Создает новый HTML-шаблон с указанным именем, ассоциированный с текущим и имеющий те же разделители. Позволяет использовать вызовы шаблонов через {{template}}
. Если шаблон с таким именем уже существует, он будет заменен.
func (*Template) Option
func (t *Template) Option(opt ...string) *Template
Устанавливает опции шаблона в формате строк “ключ=значение” или простых строк. Вызывает панику при неизвестной или неверной опции.
Поддерживаемые опции:
missingkey=default
или missingkey=invalid
- поведение по умолчанию (продолжать выполнение)
missingkey=zero
- возвращать нулевое значение для отсутствующего ключа
missingkey=error
- прерывать выполнение с ошибкой
func (*Template) Parse
func (t *Template) Parse(text string) (*Template, error)
Разбирает текст как тело шаблона для t
. Определения именованных шаблонов ({{define}}
/{{block}}
) становятся ассоциированными шаблонами. Шаблоны можно переопределять до первого вызова Execute
. Пустые определения (только пробелы/комментарии) не заменяют существующие шаблоны.
func (*Template) ParseFS
func (t *Template) ParseFS(fs fs.FS, patterns ...string) (*Template, error)
ParseFS аналогична Template.ParseFiles и Template.ParseGlob, но читает из абстрактной файловой системы fs вместо файловой системы хоста. Принимает список glob-шаблонов (большинство имен файлов работают как glob-шаблоны, соответствующие только самим себе).
func (*Template) ParseFiles
func (t *Template) ParseFiles(filenames ...string) (*Template, error)
ParseFiles парсит указанные файлы и ассоциирует полученные шаблоны с t. При ошибке парсинг останавливается и возвращается nil, иначе возвращается t. Должен быть указан хотя бы один файл.
Особенности:
- При парсинге нескольких файлов с одинаковыми именами используется последний указанный файл
- Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены
func (*Template) ParseGlob
func (t *Template) ParseGlob(pattern string) (*Template, error)
ParseGlob парсит шаблоны из файлов, соответствующих указанному шаблону (согласно семантике filepath.Match), и ассоциирует их с t. Эквивалентно вызову t.ParseFiles со списком файлов, соответствующих шаблону.
Особенности:
- Как и в ParseFiles, при дублировании имен используется последний файл
- Возвращает ошибку, если t или любой ассоциированный шаблон уже были выполнены
func (*Template) Templates
func (t *Template) Templates() []*Template
Templates возвращает срез всех шаблонов, ассоциированных с t, включая сам t.
type URL
URL инкапсулирует безопасный URL или подстроку URL (согласно RFC 3986). URL типа javascript:checkThatFormNotEditedBeforeLeavingPage()
из доверенного источника может быть использован, но по умолчанию динамические javascript:
URL фильтруются как частый вектор атак.
Внимание: Использование этого типа представляет угрозу безопасности - содержимое должно поступать из доверенного источника, так как включается в вывод шаблона без изменений.