Серверные HTML-шаблоны
В предыдущем состоянии сервер возвращал оборудование текстовыми строками. Теперь превратим тот же набор в HTML-таблицу. Данные остаются прежними: клавиатура в офисе и монитор на складе, те же ID и остатки. Меняется только представление. Такой переход позволяет отдельно понять роль шаблона: он соединяет известную разметку с данными, но не должен принимать решения о проведении складской операции.
Используем пакет html/template, который входит в стандартную библиотеку Go. Его назначение отличается от простого форматирования строки: пакет учитывает HTML-контекст значений. Это особенно важно для будущих названий, которые введёт человек. Если представить пользовательский текст как готовую разметку, обычная карточка может неожиданно получить скрипт или сломать структуру документа. В нашей таблице название должно оставаться названием.
Разметка и модель представления
Таблица описывает постоянную форму страницы, а range повторяет строку для каждого элемента среза. Внутри повторения текущим значением становится отдельный Item. Поэтому .SKU, .Name и .Quantity относятся к полям карточки, а не к глобальным переменным. Точка в шаблоне — контекст данных. Если передать вместо среза другую структуру, тот же шаблон может перестать соответствовать ожидаемой форме входа.
В состоянии lesson-03 шаблон пока задан строкой в main.go. Это полный учебный файл, поэтому скрытых файлов разметки на данном шаге нет. Позже вынесем представление в view.html, когда появятся несколько связанных фрагментов. Само размещение строки не определяет безопасность вывода: важнее, какой пакет выполняет шаблон и какие типы значений передаются внутрь.
package main
import (
"fmt"
"log"
"net/http"
"time"
"html/template"
"bytes"
)
type Item struct {
ID int64
SKU, Name, Location string
Quantity, Version int
}
var seed = []Item{
{1,"KB-001","Клавиатура","офис",12,1},
{2,"MON-001","Монитор","склад",6,1},
}
func main(){
mux:=http.NewServeMux()
view:=template.Must(template.New("page").Parse(`<html lang="ru"><meta charset="utf-8"><title>Инвентарь</title><h1>Инвентарь</h1><table><tr><th>Артикул</th><th>Название</th><th>Остаток</th></tr>{{range .}}<tr><td>{{.SKU}}</td><td>{{.Name}}</td><td>{{.Quantity}}</td></tr>{{end}}</table></html>`))
mux.HandleFunc("GET /{$}",func(w http.ResponseWriter,r *http.Request){
var body bytes.Buffer
if err:=view.Execute(&body,seed);err!=nil {http.Error(w,"Шаблон недоступен",500);return}
w.Header().Set("Content-Type","text/html; charset=utf-8")
fmt.Fprint(w,body.String())
})
server:=&http.Server{Addr:"127.0.0.1:4493",Handler:mux,ReadHeaderTimeout:5*time.Second}
log.Println("http://127.0.0.1:4493")
log.Fatal(server.ListenAndServe())
}
template.Must используется для проверки постоянного шаблона при запуске. Ошибка синтаксиса в исходной разметке означает, что разработчик подготовил неисправное приложение; прекращение запуска здесь понятнее, чем выдача первой половины страницы каждому посетителю. Это не подход для любого внешнего шаблона: если файл редактируется пользователем во время работы, понадобится явная обработка ошибки и политика принятия новой версии.
Обратите внимание на подписи столбцов. Внутреннее поле Quantity не обязано появляться в интерфейсе как английское техническое название. Пользователю нужен «Остаток». Модель представления переводит предметную информацию в удобную форму. Когда появится версия карточки, она попадёт в скрытое поле формы, потому что сотруднику обычно не нужно вручную запоминать её значение. Наличие поля в модели не требует отображения отдельной колонки.
Экранирование значения
Рассмотрим название Клавиатура <новая>. Оно должно отображаться как текст с угловыми скобками. Шаблон преобразует специальные символы в безопасное представление для соответствующего контекста. В исходном HTML ответа можно увидеть сущности, а на экране — привычную строку. Это не изменение данных карточки: экранирование относится к выдаче, исходное значение остаётся предметным текстом и не превращается в заранее подготовленную HTML-строку.
Ошибочный обход — приводить произвольное название к template.HTML, чтобы «сохранить красивую разметку». Такой тип означает доверие к значению и снимает часть защиты. Для названия оборудования это доверие не нужно. Даже если сегодня карточки задаёт разработчик, завтра форма позволит вводить их пользователю. Правило вывода лучше установить на уровне представления заранее, чем искать все места соединения строк после появления первых внешних данных.
Безопасная выдача также зависит от места вставки. Значение внутри текстовой ячейки, URL атрибута и блока JavaScript находятся в разных контекстах. В этом уроке используются только обычные текстовые ячейки. Мы не передаём целый объект в исполняемый скрипт и не строим произвольные адреса из названия. Узкий договор облегчает чтение: каждое поле имеет понятный путь от модели к конкретной части документа.
Буфер до начала ответа
В обработчике шаблон выполняется в bytes.Buffer. Если выполнение завершится ошибкой, сервер ещё не начал выдавать страницу и может вернуть отдельный статус 500. Прямая запись в ResponseWriter проще на несколько строк, но при ошибке посередине посетитель может получить обрезанный документ с уже отправленным успешным статусом. Для нашей небольшой таблицы память буфера ограничена размером списка, поэтому ясность поведения важнее ранней потоковой выдачи.
После успешного выполнения устанавливается Content-Type для HTML, затем тело передаётся клиенту. Теперь браузер интерпретирует теги и строит таблицу. Если оставить прежний text/plain, он может показать исходную разметку как текст. Если поменять только заголовок, но не сформировать корректный документ, сервер не исправит HTML автоматически. Представление и HTTP-оболочка — связанные, однако самостоятельные части ответа.
Ожидаемый результат самостоятельного прохождения — одна таблица с двумя строками. Проверять её удобно двумя способами: глазами на странице и чтением исходного ответа. Первый показывает интерпретацию браузера, второй — реальную выдачу сервера. Статья не содержит выполненного запуска или скриншота учебного приложения: результат описан по исходникам, а команды остаются инструкцией для читателя. Разделять ожидаемую и наблюдённую выдачу нужно даже в простом примере.
При расширении списка до пустого среза range не создаст ни одной строки. Это допустимо на уровне шаблона, но не объясняет человеку причину пустоты. В следующих состояниях добавим ветку else с сообщением для пустого представления. Она пригодится для фильтра, где отсутствие совпадений не означает неисправность сервера. Если вместо пустого списка произошло исключение чтения базы, обработчик должен вернуть ошибку до шаблона, а не маскировать её под отсутствие оборудования.
Сейчас все данные находятся в памяти исходного файла и исчезают как изменяемое состояние при остановке процесса. Таблица уже отделена от ответа, но постоянства ещё нет. Следующий шаг сохранит карточки в SQLite. Форма модели представления останется узнаваемой, а источником её строк станет запрос с определённым порядком и явной обработкой ошибок чтения.
Шаблон не должен самостоятельно обращаться к хранилищу за каждой ячейкой. В нашем варианте он получает уже подготовленный срез и только отображает его. Если перенести поиск карточки внутрь функции шаблона, количество обращений к данным станет зависеть от разметки, а ошибки чтения окажутся посередине вывода. Разработчику будет сложнее объяснить, почему добавление столбца неожиданно замедлило страницу. Передача модели целиком сохраняет ясную последовательность: получить сведения, проверить результат, подготовить HTML и начать HTTP-ответ.
Исходники урока
Оглавление серии · Скачать исходники. Полное самостоятельное состояние находится в lesson-03. Учебные приложения, зависимости, база и проверки при подготовке не запускались; описанный результат ожидается по исходникам. Материалы пока находятся в локальной редакционной версии.
Первичные источники: документация 1.