Alex Edwards Let's Go часть 1
Categories:
Основы Web приложения
package main
import (
"log" //пакет для логирования
"net/http" //пакет для создания web приложений
)
func home(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Hello from Snippetbox"))
}
//основная функция, `home` которая получает на вход указатель кто слушает канал:
//w http.ResponseWriter чтобы выводить значения в web
//r *http.Request - структура, которую получаем на вход
func main() {
mux := http.NewServeMux()
//указатель на web сервер
mux.HandleFunc("/", home)
//обработчик запросов
log.Println("Starting server on :4000")
//логируем подключение
err := http.ListenAndServe(":4000", mux)
//слушаем порт
log.Fatal(err)
}
type ResponseWriter interface {
// Header возвращает карту заголовков, которая будет отправлена
// [ResponseWriter.WriteHeader].
Header() Header
// Write записывает данные в соединение как часть HTTP-ответа.
Write([]byte) (int, error)
// WriteHeader отправляет заголовок ответа HTTP с указанным
// кодом состояния.
WriteHeader(statusCode int)
}
type Request struct {
// Method задает метод HTTP (GET, POST, PUT и т.д.).
Method string
// Для клиентских запросов URL's Host указывает сервер, к которому нужно // подключиться.
URL *url.URL
// Версия протокола для входящих запросов сервера.
Proto string // «HTTP/1.0»
ProtoMajor int // 1
ProtoMinor int // 0
// Заголовок содержит поля заголовка запроса, которые либо получены
// сервером, либо должны быть отправлены клиентом.
//
// Host: example.com
// accept-encoding: gzip, deflate
// Accept-Language: en-us
// fOO: Bar
// foo: two
Request Header
// Body - это тело запроса.
Body io.ReadCloser
// GetBody определяет необязательную функцию для возврата новой копии Body.
GetBody func() (io.ReadCloser, error)
// ContentLength записывает длину связанного содержимого.
ContentLength int64
// TransferEncoding перечисляет кодировки передачи данных от крайних к внутренней.
TransferEncoding []string
Close bool
// Для серверных запросов поле Host указывает хост, на котором URL-адрес.
Host String
// Form содержит разобранные данные формы
Form url.Values
// PostForm содержит разобранные данные формы из параметров тела PATCH, POST
// или параметров тела PUT.
PostForm url.Values
// MultipartForm - это разобранная многочастная форма, включая загрузку файлов.
MultipartForm *multipart.Form
// Trailer определяет дополнительные заголовки, которые отправляются после запроса
// body.
Trailer Header
// RemoteAddr позволяет HTTP-серверам и другому программному обеспечению записывать сетевой адрес, отправивший запрос
RemoteAddr String
// RequestURI - это немодифицированный запрос-цель в
RequestURI String
// TLS позволяет HTTP-серверам и другому программному обеспечению записывать информацию о TLS-соединении
TLS *tls.ConnectionState
// Cancel - необязательный канал, закрытие которого указывает на то, что клиентский
// запрос следует считать отмененным.
Cancel <-chan struct{}
// Response - это ответ перенаправления, который вызвал этот запрос.
Response *Response
// Pattern - шаблон [ServeMux], который соответствует запросу.
Pattern string
// содержит отфильтрованные или неотфильтрованные поля
}
Расширение обработчиков
package main
import (
"log"
"net/http"
)
func home(w http.ResponseWriter, r *http.Request) { w
.Write([]byte("Hello from Snippetbox"))
} создаем функции обработчики каждого адреса
func snippetView(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Display a specific snippet..."))
} // для теста просто выводим что-нибудь
func snippetCreate(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Create a new snippet...")) }
func main() {
// Register the two new handler functions and corresponding URL patterns with // the servemux, in exactly the same way that we did before.
mux := http.NewServeMux()
mux.HandleFunc("/", home)
mux.HandleFunc("/snippet/view", snippetView)
mux.HandleFunc("/snippet/create", snippetCreate)
// обрабатываем каждый адрес из браузера и запускаем функцию обработчик
log.Println("Starting server on :4000")
err := http.ListenAndServe(":4000", mux)
log.Fatal(err)
}
Типы адресов в обработчике
/snippet/view
— фиксированный адрес/snippet/view/
— не фиксированный адрес и под ним обрабатывается поддерево- Для корня проверка если указано что-то отличающееся от
/
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Write([]byte("Hello from Snippetbox"))
DefaultServeMux
var DefaultServeMux = NewServeMux()
это неявная переменная, которая хранит указатель на сервер и используется глобально. Поэтому сервера нужно создавать локально внутри main
У маршрутизатора Go могут быть проблемы с обработкой переменных
Но это по всей видимости в старых версиях. Новые могут больше и есть дополнительные модули, которые полноценно обрабатывают строку запроса
Отправка статуса
func snippetCreate(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" { // просто покажет каким методом отправили запрос
w.Header().Set("Allow", "POST")` //- эта настройка подскажет в ответе, какой метод разрешен
w.WriteHeader(405) // отправим код статуса в заголовке ответа
w.Write([]byte("Method Not Allowed")) // и просто напишем сообщение в случае неправильного кода
return
}
w.Write([]byte("Create a new snippet...")) //это сообщение успеха
}
curl -i -X POST http://localhost:4000/snippet/create // это для тестирования из командной строки
w.Header().Set("Allow", "POST")
- эта настройка подскажет в ответе, какой метод разрешен
w.Write([]byte("Method Not Allowed"))
у этой команды есть альтернатива более приемлемая: http.Error(w, "Method Not Allowed", 405)
и еще есть специальные константы с указанием статусов: http.StatusMethodNotAllowed
вместо 405
Статусы
const (
StatusContinue = 100 // RFC 9110, 15.2.1
StatusSwitchingProtocols = 101 // RFC 9110, 15.2.2
StatusProcessing = 102 // RFC 2518, 10.1
StatusEarlyHints = 103 // RFC 8297
StatusOK = 200 // RFC 9110, 15.3.1
StatusCreated = 201 // RFC 9110, 15.3.2
StatusAccepted = 202 // RFC 9110, 15.3.3
StatusNonAuthoritativeInfo = 203 // RFC 9110, 15.3.4
StatusNoContent = 204 // RFC 9110, 15.3.5
StatusResetContent = 205 // RFC 9110, 15.3.6
StatusPartialContent = 206 // RFC 9110, 15.3.7
StatusMultiStatus = 207 // RFC 4918, 11.1
StatusAlreadyReported = 208 // RFC 5842, 7.1
StatusIMUsed = 226 // RFC 3229, 10.4.1
StatusMultipleChoices = 300 // RFC 9110, 15.4.1
StatusMovedPermanently = 301 // RFC 9110, 15.4.2
StatusFound = 302 // RFC 9110, 15.4.3
StatusSeeOther = 303 // RFC 9110, 15.4.4
StatusNotModified = 304 // RFC 9110, 15.4.5
StatusUseProxy = 305 // RFC 9110, 15.4.6
StatusTemporaryRedirect = 307 // RFC 9110, 15.4.8
StatusPermanentRedirect = 308 // RFC 9110, 15.4.9
StatusBadRequest = 400 // RFC 9110, 15.5.1
StatusUnauthorized = 401 // RFC 9110, 15.5.2
StatusPaymentRequired = 402 // RFC 9110, 15.5.3
StatusForbidden = 403 // RFC 9110, 15.5.4
StatusNotFound = 404 // RFC 9110, 15.5.5
StatusMethodNotAllowed = 405 // RFC 9110, 15.5.6
StatusNotAcceptable = 406 // RFC 9110, 15.5.7
StatusProxyAuthRequired = 407 // RFC 9110, 15.5.8
StatusRequestTimeout = 408 // RFC 9110, 15.5.9
StatusConflict = 409 // RFC 9110, 15.5.10
StatusGone = 410 // RFC 9110, 15.5.11
StatusLengthRequired = 411 // RFC 9110, 15.5.12
StatusPreconditionFailed = 412 // RFC 9110, 15.5.13
StatusRequestEntityTooLarge = 413 // RFC 9110, 15.5.14
StatusRequestURITooLong = 414 // RFC 9110, 15.5.15
StatusUnsupportedMediaType = 415 // RFC 9110, 15.5.16
StatusRequestedRangeNotSatisfiable = 416 // RFC 9110, 15.5.17
StatusExpectationFailed = 417 // RFC 9110, 15.5.18
StatusTeapot = 418 // RFC 9110, 15.5.19 (Unused)
StatusMisdirectedRequest = 421 // RFC 9110, 15.5.20
StatusUnprocessableEntity = 422 // RFC 9110, 15.5.21
StatusLocked = 423 // RFC 4918, 11.3
StatusFailedDependency = 424 // RFC 4918, 11.4
StatusTooEarly = 425 // RFC 8470, 5.2.
StatusUpgradeRequired = 426 // RFC 9110, 15.5.22
StatusPreconditionRequired = 428 // RFC 6585, 3
StatusTooManyRequests = 429 // RFC 6585, 4
StatusRequestHeaderFieldsTooLarge = 431 // RFC 6585, 5
StatusUnavailableForLegalReasons = 451 // RFC 7725, 3
StatusInternalServerError = 500 // RFC 9110, 15.6.1
StatusNotImplemented = 501 // RFC 9110, 15.6.2
StatusBadGateway = 502 // RFC 9110, 15.6.3
StatusServiceUnavailable = 503 // RFC 9110, 15.6.4
StatusGatewayTimeout = 504 // RFC 9110, 15.6.5
StatusHTTPVersionNotSupported = 505 // RFC 9110, 15.6.6
StatusVariantAlsoNegotiates = 506 // RFC 2295, 8.1
StatusInsufficientStorage = 507 // RFC 4918, 11.5
StatusLoopDetected = 508 // RFC 5842, 7.2
StatusNotExtended = 510 // RFC 2774, 7
StatusNetworkAuthenticationRequired = 511 // RFC 6585, 6
)
Заголовок запроса
Автоматически устанавливаются три переменные запроса:
Date
и Content-Length
и Content-Type
.
Content-Length
определяется автоматически, но лучше установить явно:
w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Alex"}`))
Методы управления заголовком
- w.Header().Set()
- w.Header().Add()
- w.Header().Del()
- w.Header().Get()
- w.Header().Values()
w.Header().Set("Cache-Control", "public, max-age=31536000")
w.Header().Add("Cache-Control", "public")
w.Header().Add("Cache-Control", "max-age=31536000")
w.Header().Del("Cache-Control")
w.Header().Get("Cache-Control")
w.Header().Values("Cache-Control")
Канонические имена в заголовке
Все имена в заголовке преобразуются в канонические: Первая буква с заглавной
Если нужно отменить преобразование:
w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}
Подавление системных заголовков
w.Header()["Date"] = nil
потому-что DEL не удаляет их
Query
/snippet/view?id=1 // обработка строки запроса
id, err := strconv.Atoi(r.URL.Query().Get("id"))
if err != nil || id < 1 {
http.NotFound(w, r)
return
}
Структура проекта
- Main оставить в одном файле
- Handler вынести в отдельный файл с такой же структурой
internal
создадим директорию для зависимых пакетов для проекта
html шаблоны
ui/html/pages/home.tmpl
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>Home - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
html/template
- управляет шаблонами html из GO
ts, err := template.ParseFiles("./ui/html/pages/home.tmpl")
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
загружаем и парсим шаблон. Теперь он в переменной ts
err = ts.Execute(w, nil) //отправляет шаблон в поток w
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
}
Шаблоны отдельно
Делаем составные шаблоны
- Базовый шаблон base.tmpl
Задает основную структуру страницы:
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
{{define "base"}}
- определяет основной блок, внутри вся структура
{{template "title" .}}
- заголовок указанный в переменной title, которую потом подставим
{{template "main" .}}
- название другого блока с именем main
чтобы вставить template
их нужно объявить как define
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
Создание слайса с шаблонами
files := []string{
"./ui/html/base.tmpl",
"./ui/html/pages/home.tmpl",
}
что примечательного в этом коде: здесь нужно перечислить все шаблоны с их путями, которые будут собираться для вывода страницы
и немного поменялся парсер, теперь ему передадим не один шаблон, а слайсер с шаблонами
ts, err := template.ParseFiles(files...)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
и поменяется метод для сборки всех шаблонов в одну страницу:
err = ts.ExecuteTemplate(w, "base", nil)
if err != nil {
log.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
}
ts.ExecuteTemplate(w, "base", nil)
- указываем главный шаблон base
, в который будут собираться остальные
в итоге получили такой шаблон:
Итоговый шаблон
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<!-- Invoke the navigation template -->
{{template "nav" .}}
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
Block
Это похоже на template, только является необязательным
{{define "base"}}
<h1>An example template</h1>
{{block "sidebar" .}}
<p>My default sidebar content</p>
{{end}}
{{end}}
Еще есть функционал для встраивания файлов прямо в код с пакетом: embed
Статические файлы
http.FileServer
- встроенный обработчик статическийх файлов
fileServer := http.FileServer(http.Dir("./ui/static/"))
для подключения обработчика статических файлов необходимо:
mux.Handle("/static/", http.StripPrefix("/static", fileServer)) //http.StripPrefix позволяет правильно работать в адресной строке с именами файлов
Что можно положить в статику:
- CSS файлы
- JS файлы
Пример
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
<!-- Link to the CSS stylesheet and favicon -->
<link rel='stylesheet' href='/static/css/main.css'>
<link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
<!-- Also link to some fonts hosted by Google -->
<link rel='stylesheet' href='https://fonts.googleapis.com/css?family=Ubuntu+Mono:400,700'>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
{{template "nav" .}}
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
<!-- And include the JavaScript file -->
<script src="/static/js/main.js" type="text/javascript"></script>
</body>
</html>
{{end}}
<script src="/static/js/main.js" type="text/javascript"></script>
- загрузит статический файл с JS
<link rel='stylesheet' href='/static/css/main.css'>
- загрузит CSS на страницу
Hundle что делает
Отступление с объяснением Hundle
В пакете net/http
языка Go есть два способа зарегистрировать обработчик (handler) для определённого пути (path):
func (*ServeMux) Handle
– это метод структурыServeMux
.func Handle
– это функция уровня пакета, которая использует дефолтныйServeMux
.
Разница между ними:
Критерий | (*ServeMux).Handle |
http.Handle |
---|---|---|
Тип | Метод (ServeMux.Handle ) |
Функция (http.Handle ) |
Принадлежность | Работает с конкретным ServeMux (роутером) |
Работает с дефолтным ServeMux |
Где используется | Когда нужно кастомное мультиплексирование | Для простых случаев (использует DefaultServeMux ) |
Пример использования | mux := http.NewServeMux(); mux.Handle("/", handler) |
http.Handle("/", handler) |
Примеры:
1. Использование (*ServeMux).Handle
(кастомный ServeMux
)
mux := http.NewServeMux() // создаём свой мультиплексор
mux.Handle("/path", myHandler) // регистрируем обработчик
http.ListenAndServe(":8080", mux) // запускаем сервер с этим мультиплексором
2. Использование http.Handle
(дефолтный ServeMux
)
http.Handle("/path", myHandler) // регистрируем обработчик в DefaultServeMux
http.ListenAndServe(":8080", nil) // nil означает использование DefaultServeMux
Важно:
- Если вы используете
http.Handle
, то обработчик регистрируется вDefaultServeMux
. - Если вы хотите кастомный роутер (например, для middleware, изоляции путей и т. д.), нужно использовать
NewServeMux()
и(*ServeMux).Handle
. - В продакшене часто избегают
DefaultServeMux
из-за возможных конфликтов (например, если несколько библиотек тоже регистрируют пути в нём).
Вывод:
(*ServeMux).Handle
– для кастомных роутеров.http.Handle
– для быстрого использования дефолтного роутера.
Hundler для чего нужен
Функция (*ServeMux).Handler
в пакете net/http
используется для поиска обработчика (http.Handler
) и шаблона пути (pattern
), который соответствует переданному HTTP-запросу (*http.Request
).
📌 Сигнатура функции:
func (mux *ServeMux) Handler(r *Request) (h Handler, pattern string)
r *Request
– входящий HTTP-запрос.- Возвращает:
h Handler
– обработчик, который должен обработать запрос.pattern string
– шаблон пути, который совпал с URL запроса (например,"/users/"
).
🔹 Когда используется?
- Когда нужно вручную определить обработчик для запроса (например, для кастомной логики маршрутизации).
- В middleware, чтобы проверить, какой обработчик будет вызван для запроса.
- Для логирования или отладки (чтобы узнать, какой
pattern
соответствует запросу).
🔹 Примеры использования
1️⃣ Пример 1: Получение обработчика для запроса
package main
import (
"fmt"
"net/http"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/hello", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "Hello, World!")
})
// Создаём тестовый запрос
req, _ := http.NewRequest("GET", "http://example.com/hello", nil)
// Получаем обработчик и шаблон пути
handler, pattern := mux.Handler(req)
fmt.Println("Handler:", handler != nil) // true
fmt.Println("Pattern:", pattern) // "/hello"
}
Вывод:
2️⃣ Пример 2: Использование в middleware
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Получаем обработчик и шаблон до вызова next
handler, pattern := http.DefaultServeMux.Handler(r)
log.Printf("Request: %s %s → Handler: %T, Pattern: %s", r.Method, r.URL.Path, handler, pattern)
next.ServeHTTP(w, r)
})
}
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home"))
})
http.ListenAndServe(":8080", loggingMiddleware(http.DefaultServeMux))
}
При запросе GET /
в логах появится:
🔹 Как работает внутри?
- Проверяет
Host
(если задан вServeMux
). - Ищет точное совпадение пути (например,
/users
). - Ищет самое длинное совпадение по префиксу (если путь зарегистрирован с
/users/
, то запрос/users/123
тоже сработает). - Если ничего не найдено – возвращает
NotFoundHandler
(который отвечает с404
).
🔹 Отличие от http.Handler()
(*ServeMux).Handler
– метод конкретногоServeMux
.http.Handler()
– работает сDefaultServeMux
.
Пример:
// С кастомным ServeMux
mux := http.NewServeMux()
handler, pattern := mux.Handler(req)
// С DefaultServeMux
handler, pattern := http.DefaultServeMux.Handler(req)
// Или просто:
handler, pattern := http.Handler(req) // работает с DefaultServeMux
🔹 Вывод
(*ServeMux).Handler
полезен, когда нужно динамически определить обработчик для запроса.- Используется в middleware, кастомных роутерах, логировании.
- Возвращает как сам обработчик, так и шаблон пути, что помогает в отладке.
ServeHTTP объяснение
Функция (*ServeMux).ServeHTTP
— это основной метод обработки HTTP-запросов в ServeMux
, который реализует интерфейс http.Handler
. Она отвечает за:
- Поиск подходящего обработчика (на основе URL пути)
- Вызов этого обработчика (или возврат
404 Not Found
)
🔍 Развёрнутое объяснение
📌 Сигнатура
func (mux *ServeMux) ServeHTTP(w http.ResponseWriter, r *http.Request)
w ResponseWriter
— интерфейс для записи HTTP-ответа.r *Request
— входящий HTTP-запрос.
🔧 Как работает? (Mermaid-схема)
flowchart TD A[Запрос поступает в ServeMux.ServeHTTP] --> B{Есть Host в URL?} B -->|Да| C[Ищем обработчик по host + path] B -->|Нет| D[Ищем обработчик только по path] C --> E{Найден обработчик?} D --> E E -->|Да| F[Вызываем handler.ServeHTTP(w, r)] E -->|Нет| G[Возвращаем 404 Not Found]
Пошаговый алгоритм:
- Проверяет
Host
(если вServeMux
есть пути с указанием хоста, например"example.com"
). - Ищет точное совпадение пути (например,
/users
). - Ищет самое длинное совпадение по префиксу (если путь зарегистрирован как
/users/
, то/users/123
тоже сработает). - Если обработчик не найден — вызывает
NotFoundHandler
(отправляет404
).
📂 Пример кода
package main
import (
"net/http"
)
func main() {
mux := http.NewServeMux()
// Регистрируем обработчики
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/about", aboutHandler)
// Запускаем сервер
http.ListenAndServe(":8080", mux)
}
func homeHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Home Page"))
}
func aboutHandler(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("About Page"))
}
🛠 Что происходит при запросе?
-
Запрос
GET /
ServeHTTP
находит точное совпадение с/
→ вызываетhomeHandler
.
-
Запрос
GET /about
- Совпадение с
/about
→ вызываетaboutHandler
.
- Совпадение с
-
Запрос
GET /unknown
- Совпадений нет → возвращает
404
.
- Совпадений нет → возвращает
⚙️ Кастомизация ServeMux
Можно переопределить ServeHTTP
для своей логики (например, для middleware):
type CustomMux struct {
*http.ServeMux
}
func (m *CustomMux) ServeHTTP(w http.ResponseWriter, r *http.Request) {
// Логирование перед вызовом обработчика
log.Println("Request:", r.URL.Path)
m.ServeMux.ServeHTTP(w, r) // Вызов оригинального ServeHTTP
}
func main() {
mux := &CustomMux{http.NewServeMux()}
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Write([]byte("Custom Mux!"))
})
http.ListenAndServe(":8080", mux)
}
🔥 Ключевые особенности
-
Иерархия путей:
/images/
совпадёт с/images/logo.png
./images
(без слеша) — только с точным совпадением.
-
Host-specific пути:
mux.Handle("example.com/", handler) // Сработает только для example.com
-
Дефолтный обработчик 404:
Можно заменить черезmux.NotFound = customHandler
.
🎯 Вывод
ServeHTTP
— ядро роутинга вnet/http
.- Автоматически обрабатывает префиксы, хосты и 404.
- Может быть переопределён для кастомной логики.
Что нужно сделать,чтобы получить Handle
- Создать структуру
type home struct {}
- или любой объект - Создать функцию обработчик с вызовом
ServeHTTP
func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("This is my home page")) }
- Используем в коде
mux := http.NewServeMux()
mux.Handle("/", &home{})
ВСЁ!!!
А это синтетический сахар для обертывания обычных функций в Handle
mux := http.NewServeMux()
mux.HandleFunc("/", home) // home здесь обычная функция
http.ListenAndServe()
Это собственно сам сервер у которого тоже есть такой же метод ServeHTTP и который отправляет на него запросы по цепочке
Работаем с флагами командной строки
Пакет: flag
addr := flag.String("addr", ":4000", "HTTP network address") // прочитали адрес
flag.Parse()
...
log.Printf("Starting server on %s", *addr) //вывели на печать
err := http.ListenAndServe(*addr, mux) //слушаем порт, который получили из адресной строки
log.Fatal(err)
Флаги могут преобразовываться сразу в нужный тип:
- flag.String()
- flag.Int(),
- flag.Bool()
- flag.Float64().
help автоматически
для команды help автоматически сформируется все параметры вместе с подсказками
os.Getenv
Прочитает значение из переменной среды:
addr := os.Getenv("SNIPPETBOX_ADDR")
Структура флагов
type config struct {
addr string
staticDir string
}
создаем структуру для флагов и определяем переменные из структуры:
var cfg config
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()
для этого используем функции:
- flag.StringVar(),
- flag.IntVar(),
- flag.BoolVar()
Логирование
- Стандартный лог выводит в терминал
log.Printf("Starting server on %s", *addr) // Information message
err := http.ListenAndServe(*addr, mux)
log.Fatal(err) // Error message
Продвинутый лог
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime) // создали макет лога для сообщений
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile) //макет лога для ошибок
... //вывод лога
infoLog.Printf("Starting server on %s", *addr)
err := http.ListenAndServe(*addr, mux)
errorLog.Fatal(err)
$ go run ./cmd/web
INFO 2022/01/29 16:00:50 Starting server on :4000
ERROR 2022/01/29 16:00:50 main.go:37: listen tcp :4000: bind: address already in use
exit status 1
log.Llongfile
, log.Lshortfile
— эти флаги определяют полноту выводимых данных
перенаправим вывод журнала в файлы
$ go run ./cmd/web >>/tmp/info.log 2>>/tmp/error.log
Логирование работы сервера
- Создать структуру для параметров сервера HTTP
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
srv := &http.Server{
Addr: *addr,
ErrorLog: errorLog,
Handler: mux,
}
// включаем сервер с обработчиком ошибок
err := srv.ListenAndServe()
Логирование в файл
f, err := os.OpenFile("/tmp/info.log", os.O_RDWR|os.O_CREATE, 0666)
if err != nil {
log.Fatal(err)
}
defer f.Close()
infoLog := log.New(f, "INFO\t", log.Ldate|log.Ltime)
Логирование во всех зависимостях приложения
Создать структуру приложения с его переменными
type application struct {
errorLog *log.Logger
infoLog *log.Logger
}
в main.go задать значения:
infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
app := &application{
errorLog: errorLog,
infoLog: infoLog,
}
и использовать во всех зависимостях
так в обработчиках добавляем зависимость от структуры:
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
Вариант использования пакета config
func main() {
app := &config.Application{
ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
}
mux.Handle("/", examplePackage.ExampleHandler(app))
}
func ExampleHandler(app *config.Application) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
...
ts, err := template.ParseFiles(files...)
if err != nil {
app.ErrorLog.Println(err.Error())
http.Error(w, "Internal Server Error", 500)
return
}
...
}
}
пакет позволяет применять конфигурацию для всех зависимостей
Динамические данные в шаблоне
Объявление внутреннего модуля в проекте
в проекте может быть несколько внутренних и не только внутренних модулей. В нашем случае внутренний модуль лежит в папке /internal/models
. Учитывая, что основной проект называется: snippetbox.alexedwards.net
, то полный путь к модулю будет: "snippetbox.alexedwards.net/internal/models"
package main
import (
"errors"
"fmt"
"html/template" // Uncomment import
"net/http"
"strconv"
"snippetbox.alexedwards.net/internal/models"
)
Для работы динамических шаблонов нужно:
- Создать шаблоны
- Выстроить иерархию внутри них (каждый блок и раздел лучше делать в отдельном шаблоне и соединять в один, например
base
) - Создать массив строк с наименованием шаблонов и путей к ним
files := []string{
"./ui/html/base.tmpl",
"./ui/html/partials/nav.tmpl",
"./ui/html/pages/view.tmpl",
}
- Пропарсить шаблоны
ts, err := template.ParseFiles(files...)
if err != nil {
app.serverError(w, err)
return
}
- Собрать html из шаблонов
err = ts.ExecuteTemplate(w, "base", snippet) //собираем в шаблоне base и передаем в него структуру данных snippet
if err != nil {
app.serverError(w, err)
}
Базовые понятия работы с шаблонами html/template
Для работы с сущностями создадим структуру в дополнительном модуле.
type Snippet struct {
ID int
Title string
Content string
Created time.Time
Expires time.Time
}
Эта структура будет работать с базой данных и шаблонами
.
— весь контекст переданный в этот блок- Передать структуру в шаблон:
err = ts.ExecuteTemplate(w, "base", snippet)
- Использовать поля структуры в шаблоне
{{.Title}}
и т.д.
{{define "title"}}Snippet #{{.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
</div>
{{end}}
Особенности пакета template
В структуру шаблонов можно передавать только одну переменную. Поэтому для передачи нескольких переменных их нужно собрать всех в одну структуру.- Создать структуру
type templateData struct {
Snippet *models.Snippet
}
- Выполнить все действия, описанные выше по парсингу шаблонов
- Создать экземпляр структуры данных
data := &templateData{
Snippet: snippet,
}
- Передать данные в генератор html кода
err = ts.ExecuteTemplate(w, "base", data)
if err != nil {
app.serverError(w, err)
}
- В шаблоне значения полей тогда будут вызываться с указанием полного пути поля:
{{.Snippet.ID}}
или{{.Snippet.Title}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Snippet.Title}}</strong>
<span>#{{.Snippet.ID}}</span>
</div>
<pre><code>{{.Snippet.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Snippet.Created}}</time>
<time>Expires: {{.Snippet.Expires}}</time>
</div>
</div>
{{end}}
Вложенные структуры в шаблонах
Объявили шаблон
{{define "base"}}
...
{{end}}
Использование шаблона
В основном шаблоне можно указывать другие шаблоны, которые определены в других файлах. В других файлах может быть определено несколько шаблонов в каждом файле.
{{define "title"}}Home{{end}}
{{define "main"}}
<h2>Latest Snippets</h2>
<p>There's nothing to see here yet!</p>
{{end}}
Вставка встроенного шаблона в точку основного шаблона
{{template "title" .}}
{{define "base"}}
<!doctype html>
<html lang='en'>
<head>
<meta charset='utf-8'>
<title>{{template "title" .}} - Snippetbox</title>
</head>
<body>
<header>
<h1><a href='/'>Snippetbox</a></h1>
</header>
<main>
{{template "main" .}}
</main>
<footer>Powered by <a href='https://golang.org/'>Go</a></footer>
</body>
</html>
{{end}}
{{block}}…{{end}}
{{define "base"}}
<h1>An example template</h1>
{{block "sidebar" .}}
<p>My default sidebar content</p>
{{end}}
{{end}}
этот параметр можем использовать для передачи значений по умолчанию. Если нет в этом блоке данных, то он и не выведется, в отличии от {{template}}, который будет требовать обязательного вывода контента.
Вызов методов к полям
{{.Snippet.Created.Weekday}}
к нашему полю .Snippet.Created
вызовем метод Weekday
или {{.Snippet.Created.AddDate 0 6 0}}
{{if .Foo}} C1 {{else}} C2 {{end}}
{{with .Foo}} C1 {{else}} C2 {{end}}
{{define "title"}}Snippet #{{.Snippet.ID}}{{end}}
{{define "main"}}
{{with .Snippet}}
<div class='snippet'>
<div class='metadata'>
<strong>{{.Title}}</strong>
<span>#{{.ID}}</span>
</div>
<pre><code>{{.Content}}</code></pre>
<div class='metadata'>
<time>Created: {{.Created}}</time>
<time>Expires: {{.Expires}}</time>
</div>
</div>
{{end}}
{{end}}
собственно метод with
позволяет далее в этом блоке работать с полями указанной структуры без указания ее полного пути
{{range .Foo}} C1 {{else}} C2 {{end}}
range
пройдется по всем элементам .Foo
как цикл и выполнит все действия как делает with
с одним элементом
Операторы сравнения и логики
{{eq .Foo .Bar}}
{{ne .Foo .Bar}}
{{not .Foo}}
{{or .Foo .Bar}}
{{index .Foo i}}
возвращает текущий индекс массива
{{printf “%s-%s” .Foo .Bar}}
форматированная печать значений
{{len .Foo}}
{{$bar := len .Foo}}
— в этом варианте присвоит переменной длину .Foo
{{break}} {{continue}}
используется в if для разветвленной логики
Кэширование
Объяснение создания кэша
Подробный разбор map[string]*template.Template
1. Базовый анализ структуры
map[string]*template.Template
Это декларация типа в Go, представляющая:
- map - ассоциативный массив/словарь
- string - тип ключа (строка)
- *template.Template - тип значения (указатель на объект Template)
2. Компоненты:
- template.Template - тип из пакета
html/template
илиtext/template
, представляющий:- Разобранный шаблон
- Методы для выполнения шаблонов
- Ассоциированные именованные шаблоны
- Указатель (*) - используется для:
- Работы с одним экземпляром шаблона
- Избегания копирования больших структур
- Модификации шаблона после создания
3. Типичное использование:
templates := make(map[string]*template.Template)
// Добавление шаблона
templates["home"] = template.Must(template.ParseFiles("home.html"))
templates["about"] = template.Must(template.ParseFiles("about.html"))
// Использование
err := templates["home"].Execute(w, data)
4. Преимущества такого подхода:
-
Организация шаблонов:
- Группировка по функциональности (страницы, компоненты)
- Быстрый доступ по имени
-
Производительность:
- Парсинг шаблонов один раз при инициализации
- Кэширование готовых шаблонов
-
Гибкость:
- Динамический выбор шаблона
- Возможность горячей перезагрузки
5. Пример расширенного использования:
// Инициализация кэша шаблонов
var templateCache = map[string]*template.Template{
"home": parseTemplate("home.html", "layout.html"),
"about": parseTemplate("about.html", "layout.html"),
}
func parseTemplate(files ...string) *template.Template {
ts, err := template.ParseFiles(files...)
if err != nil {
log.Fatal(err)
}
return ts
}
// Обработчик HTTP
func homeHandler(w http.ResponseWriter, r *http.Request) {
data := struct{ Title string }{Title: "Главная"}
err := templateCache["home"].ExecuteTemplate(w, "layout", data)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
}
}
6. Особенности работы:
- Потокобезопасность: Шаблоны безопасны для конкурентного использования после создания
- Жизненный цикл: Обычно создаются при старте приложения
- Ошибки: Проверка должна быть при:
- Создании мапы
- Парсинге шаблонов
- Выполнении шаблонов
7. Альтернативные подходы:
-
sync.Map - для конкурентного доступа
var templates sync.Map templates.Store("home", template.Must(template.ParseFiles("home.html")))
-
Вложенные мапы - для сложных структур:
map[string]map[string]*template.Template
Такой подход широко используется в веб-фреймворках и приложениях для эффективного управления шаблонами.
создали функцию, которая будет кэшировать шаблоны в памяти map
package main
import (
"html/template" // New import
"path/filepath" // New import
"snippetbox.alexedwards.net/internal/models"
)
...
func newTemplateCache() (map[string]*template.Template, error) {
// Initialize a new map to act as the cache.
cache := map[string]*template.Template{}
// Use the filepath.Glob() function to get a slice of all filepaths that
// match the pattern "./ui/html/pages/*.tmpl". This will essentially gives
// us a slice of all the filepaths for our application 'page' templates
// like: [ui/html/pages/home.tmpl ui/html/pages/view.tmpl]
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
if err != nil {
return nil, err
}
// Loop through the page filepaths one-by-one.
for _, page := range pages {
// Extract the file name (like 'home.tmpl') from the full filepath
// and assign it to the name variable.
name := filepath.Base(page)
// Create a slice containing the filepaths for our base template, any
// partials and the page.
files := []string{
"./ui/html/base.tmpl",
"./ui/html/partials/nav.tmpl",
page,
}
// Parse the files into a template set.
ts, err := template.ParseFiles(files...)
if err != nil {
return nil, err
}
// Add the template set to the map, using the name of the page
// (like 'home.tmpl') as the key.
cache[name] = ts
}
// Return the map.
return cache, nil
}
в структуру приложения добавим поле
type application struct {
errorLog *log.Logger
infoLog *log.Logger
snippets *models.SnippetModel
templateCache map[string]*template.Template //это кэш
}
инициализация кэш
// Initialize a new template cache...
templateCache, err := newTemplateCache() //вызываем функцию и создаем кэш
if err != nil {
errorLog.Fatal(err)
}
создаем экземпляр приложения app с кэшем
app := &application{
errorLog: errorLog,
infoLog: infoLog,
snippets: &models.SnippetModel{DB: db},
templateCache: templateCache,
}
выдача кэшированных страниц
создадим функцию render
package main
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
// Retrieve the appropriate template set from the cache based on the page
// name (like 'home.tmpl'). If no entry exists in the cache with the
// provided name, then create a new error and call the serverError() helper
// method that we made earlier and return.
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, err)
return
}
// Write out the provided HTTP status code ('200 OK', '400 Bad Request'
// etc).
w.WriteHeader(status)
// Execute the template set and write the response body. Again, if there
// is any error we call the the serverError() helper.
err := ts.ExecuteTemplate(w, "base", data)
if err != nil {
app.serverError(w, err)
}
}
в эту функцию передаем название страницы и получаем готовый html код, только теперь уже из кэша
пример функции обращения к кэшу
func (app *application) home(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
app.notFound(w)
return
}
snippets, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
// Use the new render helper.
app.render(w, http.StatusOK, "home.tmpl", &templateData{
Snippets: snippets,
})
}
полный код создания кэша
package main
...
func newTemplateCache() (map[string]*template.Template, error) {
cache := map[string]*template.Template{}
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
if err != nil {
return nil, err
}
for _, page := range pages {
name := filepath.Base(page)
// Parse the base template file into a template set.
ts, err := template.ParseFiles("./ui/html/base.tmpl")
if err != nil {
return nil, err
}
// Call ParseGlob() *on this template set* to add any partials.
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
if err != nil {
return nil, err
}
// Call ParseFiles() *on this template set* to add the page template.
ts, err = ts.ParseFiles(page)
if err != nil {
return nil, err
}
// Add the template set to the map as normal...
cache[name] = ts
}
return cache, nil
}
Объяснение парсинга шаблонов в кэш
Развернутое объяснение функции newTemplateCache()
Эта функция создает кэш шаблонов для веб-приложения, загружая и компилируя все шаблоны страниц вместе с базовым шаблоном и частичными шаблонами (partials). Рассмотрим ее построчно:
1. Объявление функции
func newTemplateCache() (map[string]*template.Template, error)
- Назначение: Создает и возвращает кэш шаблонов
- Возвращаемые значения:
map[string]*template.Template
- кэш шаблонов, где ключ - имя файла шаблонаerror
- ошибка, если что-то пошло не так
2. Инициализация кэша
cache := map[string]*template.Template{}
- Создается пустая мапа для хранения шаблонов
3. Поиск файлов страниц
pages, err := filepath.Glob("./ui/html/pages/*.tmpl")
- Использует
filepath.Glob
для поиска всех файлов с расширением.tmpl
в директории./ui/html/pages/
- Возвращает список путей к файлам или ошибку
4. Обработка ошибок поиска
if err != nil {
return nil, err
}
- Если при поиске файлов произошла ошибка, возвращает ее
5. Цикл по найденным страницам
for _, page := range pages {
- Перебирает все найденные файлы шаблонов страниц
6. Получение имени файла
name := filepath.Base(page)
- Извлекает базовое имя файла (без пути) с помощью
filepath.Base()
- Например, для “./ui/html/pages/home.tmpl” вернет “home.tmpl”
7. Парсинг базового шаблона
ts, err := template.ParseFiles("./ui/html/base.tmpl")
- Создает новый набор шаблонов (
*template.Template
), загружая базовый шаблон - Базовый шаблон обычно содержит общую структуру HTML (doctype, head, общие скрипты/стили)
8. Обработка ошибок парсинга
if err != nil {
return nil, err
}
- Если не удалось загрузить базовый шаблон, возвращает ошибку
9. Добавление частичных шаблонов
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
- Добавляет в набор шаблонов все файлы из директории partials
- Частичные шаблоны - это повторно используемые компоненты (header, footer и т.д.)
10. Обработка ошибок добавления partials
if err != nil {
return nil, err
}
- Возвращает ошибку, если не удалось загрузить partials
11. Добавление шаблона страницы
ts, err = ts.ParseFiles(page)
- Загружает конкретный шаблон страницы в тот же набор шаблонов
- Это позволяет странице использовать базовый шаблон и partials
12. Обработка ошибок добавления страницы
if err != nil {
return nil, err
}
- Возвращает ошибку, если не удалось загрузить шаблон страницы
13. Сохранение в кэш
cache[name] = ts
- Сохраняет готовый набор шаблонов в кэш под именем файла страницы
14. Возврат результата
return cache, nil
- После обработки всех страниц возвращает заполненный кэш и nil в качестве ошибки
Особенности работы:
-
Иерархия шаблонов:
- Базовый шаблон (base.tmpl)
- Частичные шаблоны (partials/*.tmpl)
- Шаблоны страниц (pages/*.tmpl)
-
Преимущества:
- Все шаблоны парсятся один раз при старте приложения
- Готовые к использованию шаблоны хранятся в памяти
- Быстрый доступ к шаблонам по имени
-
Использование в обработчиках:
// Где-то в обработчике: err := cache["home.tmpl"].ExecuteTemplate(w, "base", data)
-
Расширения:
- Можно добавить горячую перезагрузку в dev-режиме
- Можно кэшировать не только по имени файла, но и по логическому имени страницы
Эта реализация обеспечивает удобное управление шаблонами в веб-приложении, следуя принципам DRY (Don’t Repeat Yourself) и обеспечивая хорошую производительность.
Как избежать ошибок при рендеринге динамического кода
- Делать рендер кода
- Записывать результат в буфер
- Проверять на отсутствие ошибок
- делать выдачу в
http.ResponseWriter
.
Обновленный код обработчика с промежуточной записью результата в буфер
package main
import (
"bytes" // New import
"fmt"
"net/http"
"runtime/debug"
)
...
func (app *application) render(w http.ResponseWriter, status int, page string, data *templateData) {
ts, ok := app.templateCache[page]
if !ok {
err := fmt.Errorf("the template %s does not exist", page)
app.serverError(w, err)
return
}
// инициализация буфера
buf := new(bytes.Buffer)
// записываем результат рендеринга в буфер
err := ts.ExecuteTemplate(buf, "base", data)
if err != nil {
app.serverError(w, err)
return
}
// запишем статус в заголовок ответа
w.WriteHeader(status)
// возвращаем результат успешного рендеринга
buf.WriteTo(w)
}
Динамические данные в шаблонах
- Создадим специальную структуру для передачи динамических данных в шаблон
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
}
- Создадим функцию helpers для возврата указателя на структуру с данными и расчетом динамических данных
func (app *application) newTemplateData(r *http.Request) *templateData {//получаем на вход указатель с данными запроса
return &templateData{ //и возвращаем указатель с данными ответа
CurrentYear: time.Now().Year(),// по дороге посчитаем какой текущий год
}
}
- Делаем вызов функции
render
и передаем ей на вход структуру с даннымиtemplateData
//загружаем из базы данные в переменную snippets
snippets, err := app.snippets.Latest()
if err != nil {
app.serverError(w, err)
return
}
data := app.newTemplateData(r) //создаем переменную типа templateData
data.Snippets = snippets //записываем в структуру переменной значения из базы данных
//вызываем функцию render и передаем ей структуру с изменяемыми данными
app.render(w, http.StatusOK, "home.tmpl", data)
- Повторяем такие вызовы для всех обработчиков
- В шаблонах обрабатываем данные из структуры
templateData
как {{.Snippets}} или {{.Snippet}} и текущий год как {{.CurrentYear}}
Пользовательские функции в шаблонах
- Создаем функцию, которую планируем добавить в шаблон
func humanDate(t time.Time) string {
return t.Format("02 Jan 2006 at 15:04")
}
- Создаем объект
template.FuncMap
и в нем объявляем свою функцию (напримерhumanDate()
). Фактически в переменной function создается карта: ключ - значение, где значение, это название функции, которое мы передаем в шаблон.
var functions = template.FuncMap{
"humanDate": humanDate,
}
- Метод должен быть зарегистрирован до парсинга шаблона
for _, page := range pages {
name := filepath.Base(page)
//перед парсером файла с шаблоном, передаем переменную functions с объявленными функциями
//функций может быть несколько
ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl")
if err != nil {
return nil, err
}
ts, err = ts.ParseGlob("./ui/html/partials/*.tmpl")
if err != nil {
return nil, err
}
ts, err = ts.ParseFiles(page)
if err != nil {
return nil, err
}
cache[name] = ts
}
- Используем функцию в шаблоне как и другие функции
<td>{{humanDate .Created}}</td>
или
<time>Created: {{.Created | humanDate}}</time>
или
<time>{{.Created | humanDate | printf "Created: %s"}}</time>
Объяснение порядка добавления функций в шаблон
Подробный разбор команды ts, err := template.New(name).Funcs(functions).ParseFiles("./ui/html/base.tmpl")
Эта строка кода создает и настраивает новый шаблон в Go. Разберем ее по цепочке вызовов:
1. template.New(name)
- Назначение: Создает новый пустой шаблон
- Параметр:
name
(string) - уникальное имя шаблона
- Возвращает: Указатель на
*template.Template
- Особенности:
- Имя используется для вызова шаблона через
{{template "name"}}
- Если шаблон с таким именем уже существует, он будет заменен
- Имя используется для вызова шаблона через
2. .Funcs(functions)
- Назначение: Регистрирует пользовательские функции для использования в шаблоне
- Параметр:
functions
(типtemplate.FuncMap
) - мапа функций видаmap[string]interface{}
- Пример:
functions := template.FuncMap{ "uppercase": strings.ToUpper, "add": func(a, b int) int { return a + b }, }
- Важно:
- Должен вызываться до парсинга шаблона
- Функции должны возвращать 1 или 2 значения (второе - error)
3. .ParseFiles("./ui/html/base.tmpl")
- Назначение: Парсит указанные файлы как тело шаблона
- Параметр:
- Путь к файлу шаблона (может принимать несколько файлов)
- Особенности:
- Первый файл становится “ассоциированным” с этим шаблоном
- Может содержать определения других шаблонов через
{{define}}
Возвращаемые значения:
ts
(*template.Template) - созданный шаблонerr
(error) - ошибка, если что-то пошло не так
Полный пример использования:
funcs := template.FuncMap{
"formatDate": func(t time.Time) string { return t.Format("2006-01-02") },
}
ts, err := template.New("base").Funcs(funcs).ParseFiles("./ui/html/base.tmpl")
if err != nil {
log.Fatal(err)
}
// Использование в обработчике
err = ts.Execute(w, data)
Что происходит под капотом:
- Создается новый именованный шаблон
- Регистрируются пользовательские функции
- Читается и парсится файл шаблона
- Все определения шаблонов из файла добавляются в набор
Особенности безопасности при использовании html/template
:
- Автоматическое экранирование в зависимости от контекста
- Защита от XSS-атак
- Контекстно-зависимое экранирование для HTML, JS, CSS
Типичные ошибки:
- Регистрация функций после парсинга
- Использование несуществующих путей к файлам
- Попытка использовать недопустимые имена функций (например, с точками)
Эта цепочка вызовов является идиоматическим способом создания шаблонов в Go и часто используется при инициализации веб-приложений.
Middleware
Стандартный паттерн middlware
func myMiddleware(next http.Handler) http.Handler {
fn := func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
}
return http.HandlerFunc(fn)
}
или использование анонимной функции
func myMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// TODO: Execute our middleware logic here...
next.ServeHTTP(w, r)
})
}
Объяснение middleware
Middleware в Go: кратко и понятно
Middleware — это промежуточный слой между HTTP-запросом и вашим обработчиком. Он позволяет выполнять код до или после основного обработчика (например, логирование, проверку авторизации, сжатие данных).
1. Глобальный Middleware (для всех запросов)
Если middleware добавлен до servemux
(роутера), то он будет выполняться для всех запросов.
Схема:
Запрос → Middleware → Servemux → Обработчик
Пример:
func logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("Запрос: %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r) // Передаем запрос дальше
})
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
// Middleware применяется ко ВСЕМ запросам
app := logRequest(mux)
http.ListenAndServe(":8080", app)
}
Когда использовать:
✅ Логирование
✅ Сжатие ответов (gzip)
✅ Добавление заголовков CORS
2. Локальный Middleware (для конкретных маршрутов)
Если middleware добавлен после servemux
(оборачивает конкретный обработчик), то он работает только для определенных маршрутов.
Схема:
Запрос → Servemux → Middleware → Обработчик
Пример:
func checkAuth(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if !isAuthenticated(r) {
http.Error(w, "Доступ запрещен", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r) // Пропускаем авторизованных
})
}
func main() {
mux := http.NewServeMux()
mux.Handle("/admin", checkAuth(adminHandler)) // Только для /admin
mux.HandleFunc("/", homeHandler) // Без middleware
http.ListenAndServe(":8080", mux)
}
Когда использовать:
✅ Проверка авторизации
✅ Валидация данных
✅ Лимитирование запросов (rate limiting)
Вывод:
- До
servemux
→ Middleware на все запросы. - После
servemux
→ Middleware только для выбранных маршрутов.
Это дает гибкость в управлении логикой приложения без дублирования кода. 🚀
Создадим обработчик заголовков для всех страниц
Будем возвращать шаблоны html и к каждому будем записывать правильный заголовок в ответе
Content-Security-Policy: default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com
Referrer-Policy: origin-when-cross-origin
X-Content-Type-Options: nosniff
X-Frame-Options: deny
X-XSS-Protection: 0
Объяснение HEADERS
Краткое описание HTTP-заголовков безопасности
Эти заголовки защищают веб-приложение от распространённых атак (XSS, clickjacking, MIME-sniffing и др.).
1. Content-Security-Policy
(CSP)
Назначение: Ограничивает источники загрузки ресурсов (JS, CSS, шрифты, изображения).
Пример:
Content-Security-Policy: default-src 'self'; font-src fonts.gstatic.com; style-src fonts.googleapis.com 'self'
Эффект:
- Блокирует inline-JS (
unsafe-inline
) - Разрешает шрифты только с
fonts.gstatic.com
- Стили — с
fonts.googleapis.com
и своего домена
Другие варианты:
script-src 'self' https://trusted.cdn.com
— разрешает скрипты только с указанных источниковimg-src *
— разрешает изображения с любых доменов (небезопасно!)
2. Referrer-Policy
Назначение: Контролирует, какие данные отправляются в заголовке Referer
при переходе на другой сайт.
Пример:
Referrer-Policy: origin-when-cross-origin
Эффект:
- При переходе внутри сайта (
same-origin
) передаётся полный URL. - При переходе на другой домен — только источник (без пути и параметров).
Другие варианты:
no-referrer
— вообще не отправлятьReferer
strict-origin
— всегда отправлять только домен (без протокола и пути)
3. X-Content-Type-Options: nosniff
Назначение: Запрещает браузеру “угадывать” тип контента (MIME-sniffing).
Пример:
X-Content-Type-Options: nosniff
Эффект:
- Браузер строго следует указанному
Content-Type
. - Защищает от атак, когда вредоносный файл маскируется под изображение/JS.
4. X-Frame-Options: deny
Назначение: Блокирует встраивание страницы в <iframe>
(защита от clickjacking).
Пример:
X-Frame-Options: deny
Альтернативы:
sameorigin
— разрешает iframe только с текущего домена- Устарел в пользу CSP с
frame-ancestors 'none'
5. X-XSS-Protection: 0
Назначение: Отключает встроенный XSS-фильтр браузера (устаревший механизм).
Почему 0?
- Современные браузеры используют CSP для защиты от XSS.
- Старые версии
X-XSS-Protection
иногда сами вызывали уязвимости.
Другие важные заголовки безопасности
-
Strict-Transport-Security
(HSTS)- Принудительное использование HTTPS:
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
-
Permissions-Policy
- Отключает нежелательные API (камера, геолокация и др.):
Permissions-Policy: geolocation=(), microphone=()
-
Cross-Origin-Opener-Policy
(COOP)- Защищает от атак типа Spectre:
Cross-Origin-Opener-Policy: same-origin
-
Cross-Origin-Embedder-Policy
(COEP)- Блокирует загрузку кросс-доменных ресурсов без CORS:
Cross-Origin-Embedder-Policy: require-corp
Итог
Заголовок | Защита от | Современная альтернатива |
---|---|---|
CSP |
XSS, data injection | — |
Referrer-Policy |
Утечка URL | — |
X-Content-Type-Options |
MIME-sniffing | — |
X-Frame-Options |
Clickjacking | CSP: frame-ancestors 'none' |
X-XSS-Protection |
Устаревший XSS-фильтр | CSP |
Оптимальный набор:
Content-Security-Policy: default-src 'self'; script-src 'self' https://trusted.cdn.com; frame-ancestors 'none'
Strict-Transport-Security: max-age=63072000; includeSubDomains
Referrer-Policy: strict-origin-when-cross-origin
X-Content-Type-Options: nosniff
- Создадим функцию для создания заголовков
package main
import (
"net/http"
)
func secureHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Note: This is split across multiple lines for readability. You don't
// need to do this in your own code.
w.Header().Set("Content-Security-Policy",
"default-src 'self'; style-src 'self' fonts.googleapis.com; font-src fonts.gstatic.com")
w.Header().Set("Referrer-Policy", "origin-when-cross-origin")
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "deny")
w.Header().Set("X-XSS-Protection", "0")
next.ServeHTTP(w, r)
})
}
- Создадим функцию обработчик маршрутов
package main
import "net/http"
// Update the signature for the routes() method so that it returns a
// http.Handler instead of *http.ServeMux.
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir("./ui/static/"))
mux.Handle("/static/", http.StripPrefix("/static", fileServer))
mux.HandleFunc("/", app.home)
mux.HandleFunc("/snippet/view", app.snippetView)
mux.HandleFunc("/snippet/create", app.snippetCreate)
// Pass the servemux as the 'next' parameter to the secureHeaders middleware.
// Because secureHeaders is just a function, and the function returns a
// http.Handler we don't need to do anything else.
return secureHeaders(mux)
}
- После обработки маршрута для каждой страницы будут создан заголовок
Функция логирования запросов
func (app *application) logRequest(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.infoLog.Printf("%s - %s %s %s", r.RemoteAddr, r.Proto, r.Method, r.URL.RequestURI())
next.ServeHTTP(w, r)
})
}
и для нашего случая выстроим цепочку middleware запросов
return app.logRequest(secureHeaders(mux))
полный код routes.go
package main
import "net/http"
func (app *application) routes() http.Handler {
mux := http.NewServeMux()
fileServer := http.FileServer(http.Dir("./ui/static/"))
mux.Handle("/static/", http.StripPrefix("/static", fileServer))
mux.HandleFunc("/", app.home)
mux.HandleFunc("/snippet/view", app.snippetView)
mux.HandleFunc("/snippet/create", app.snippetCreate)
// Wrap the existing chain with the logRequest middleware.
return app.logRequest(secureHeaders(mux))
}
Обработка паники в HTTP сервере
func (app *application) recoverPanic(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Create a deferred function (which will always be run in the event
// of a panic as Go unwinds the stack).
defer func() {
// Use the builtin recover function to check if there has been a
// panic or not. If there has...
if err := recover(); err != nil {
// Set a "Connection: close" header on the response.
w.Header().Set("Connection", "close")
// Call the app.serverError helper method to return a 500
// Internal Server response.
app.serverError(w, fmt.Errorf("%s", err))
}
}()
next.ServeHTTP(w, r)
})
}
Разбор HTTP-заголовка Connection: Close
и работы recover()
в Go
1. Заголовок Connection: Close
Назначение:
- Принудительно закрывает HTTP-соединение после отправки ответа.
- Сообщает клиенту, что сервер разорвёт соединение.
Особенности:
- В HTTP/1.x заголовок
Connection: Close
корректен и заставляет сервер закрыть соединение. - В HTTP/2 Go автоматически удаляет этот заголовок (чтобы не нарушать спецификацию) и вместо этого отправляет фрейм
GOAWAY
для graceful-закрытия.
Пример использования:
w.Header().Set("Connection", "Close") // Закрыть соединение после ответа
Когда применять:
- Для серверов с высокой нагрузкой, чтобы уменьшить число висящих соединений.
- В тестовых средах для принудительного сброса соединений.
2. Функция recover()
и обработка паник
Как работает recover()
:
- Возвращает значение, переданное в
panic()
(типany
). - Это значение может быть:
string
,error
, или любым другим типом.
justinas/alice Пример:
func handlePanic() {
if r := recover(); r != nil {
// Нормализация в error
err := fmt.Errorf("panic: %v", r)
app.serverError(err) // Обработка ошибки
}
}
Что происходит:
- Если вызвана
panic("oops! something went wrong")
,recover()
вернёт строку"oops! something went wrong"
. fmt.Errorf()
преобразует это значение вerror
.- Ошибка передаётся в метод
app.serverError()
для логирования/отправки клиенту.
Зачем это нужно:
- Чтобы избежать аварийного завершения программы при панике.
- Логировать ошибки и отправлять клиенту понятный ответ (например,
500 Internal Server Error
).
Важно:
recover()
работает только внутриdefer
.- Всегда проверяйте, что
recover()
вернул неnil
.
Заключительный вариант вызова middleware
return app.recoverPanic(app.logRequest(secureHeaders(mux)))
Делается обработка для всех запускаемых горутин.
Автор рекомендует пакет justinas/alice
Для удобного и читабельного управления middleware можно использовать пакет justinas/alice
Особенности:
- вместо
return myMiddleware1(myMiddleware2(myMiddleware3(myHandler)))
будем писатьreturn alice.New(myMiddleware1, myMiddleware2, myMiddleware3).Then(myHandler)
- есть методы Append и Then
myChain := alice.New(myMiddlewareOne, myMiddlewareTwo)
myOtherChain := myChain.Append(myMiddleware3)
return myOtherChain.Then(myHandler)
для нашего примера будет выглядеть:
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
// Return the 'standard' middleware chain followed by the servemux.
return standard.Then(mux)
Расширенные обработчики маршрутов
Автор предлагает вместо стандартной библиотеки Go использовать сторонних разработчиков:
- julienschmidt/httprouter,
- go-chi/chi,
- gorilla/mux.
сделав анализ современного развития этих пакетов можно прийти к выводу:
Сравнение пакетов
Вот сравнительный анализ трёх популярных HTTP-роутеров для Go (julienschmidt/httprouter, go-chi/chi, gorilla/mux) в сравнении со стандартным net/http
, а также рекомендации по их применению:
1. julienschmidt/httprouter
Отличия от net/http
:
✅ Высокая производительность – один из самых быстрых роутеров благодаря оптимизированному алгоритму сопоставления путей.
✅ Поддержка параметров в пути (например, /user/:id
) с синтаксисом :param
и *catch-all
.
✅ Четкое разделение методов HTTP (GET, POST и т. д.), что предотвращает коллизии.
❌ Нет встроенного middleware-стека (но можно подключить вручную).
❌ Менее гибкий, чем chi
и gorilla/mux
, из-за строгого подхода к роутингу.
Рекомендации:
✔ Используйте, если нужна максимальная скорость (например, в high-load API).
✔ Хорош для RESTful-сервисов с четкой структурой эндпоинтов.
✔ Не подходит, если нужна сложная логика маршрутизации или встроенная поддержка middleware.
2. go-chi/chi
Отличия от net/http
:
✅ Полная совместимость с net/http
– можно использовать стандартные http.Handler
и http.HandlerFunc
.
✅ Гибкость и простота – удобный синтаксис для группировки роутов и middleware.
✅ Встроенная поддержка middleware (например, CORS, логирование, аутентификация).
✅ Поддержка параметров в URL (/posts/{id}
) и regex-ограничений.
❌ Чуть медленнее, чем httprouter
, но разница незначительна в большинстве сценариев.
Рекомендации:
✔ Идеален для REST API и веб-приложений с middleware (аутентификация, логирование и т. д.).
✔ Подходит, если важна простота и совместимость со стандартной библиотекой.
✔ Лучший выбор для проектов, где баланс между скоростью и функциональностью критичен.
3. gorilla/mux
Отличия от net/http
:
✅ Богатые возможности маршрутизации – поддержка regex, условий на методы HTTP, query-параметров.
✅ Гибкость – можно задавать сложные правила для роутов (например, r.Host("example.com").Methods("GET")
).
✅ Поддержка middleware (через gorilla/handlers
или совместимость с http.Handler
).
❌ Медленнее, чем httprouter
и chi
.
❌ Сложнее в настройке из-за обилия возможностей.
❌ Проект в архиве (maintenance mode), но стабилен и используется в многих проектах.
Рекомендации:
✔ Хорош для legacy-проектов или сложных маршрутов с regex и условиями.
✔ Подходит, если нужен максимальный контроль над роутингом (например, поддомены, строгие условия).
✔ Лучше избегать в новых проектах (из-за статуса “архивный”), но старые проекты могут продолжать его использовать.
Сравнение со стандартным net/http
Роутер | Скорость | Гибкость | Middleware | Простота | Поддержка |
---|---|---|---|---|---|
net/http |
⚡️ Быстрый | ❌ Низкая | ❌ Нет | ✅ Простой | ✅ Активен |
httprouter |
⚡️⚡️ Очень быстрый | ❌ Жёсткий | ❌ Требует костылей | ✅ Простой | ✅ Активен |
chi |
⚡️ Быстрый | ✅ Высокая | ✅ Встроенный | ✅ Простой | ✅ Активен |
gorilla/mux |
🐢 Умеренный | ✅ Очень высокая | ✅ Через handlers |
❌ Сложный | ❌ Архивный |
Итоговые рекомендации:
- Для высоконагруженных API →
httprouter
. - Для баланса скорости и функциональности →
chi
. - Для сложных условий маршрутизации →
gorilla/mux
(но лучше мигрировать наchi
). - Для минимализма и обучения → стандартный
net/http
.
Если нужна максимальная скорость – httprouter
.
Если нужен удобный и современный роутер – chi
.
Если проект уже использует gorilla/mux
– можно оставить, но для новых проектов лучше chi
.
Чистые URL и маршрутизация на основе методов
Установка пакета julienschmidt/httprouter
,
go get github.com/julienschmidt/httprouter@v1
Синтаксис пакета:
package main
import (
"net/http"
"github.com/julienschmidt/httprouter" // New import
"github.com/justinas/alice"
)
func (app *application) routes() http.Handler {
// Initialize the router.
router := httprouter.New()
// Update the pattern for the route for the static files.
fileServer := http.FileServer(http.Dir("./ui/static/"))
router.Handler(http.MethodGet, "/static/*filepath", http.StripPrefix("/static", fileServer))
// And then create the routes using the appropriate methods, patterns and
// handlers.
router.HandlerFunc(http.MethodGet, "/", app.home)
router.HandlerFunc(http.MethodGet, "/snippet/view/:id", app.snippetView)
router.HandlerFunc(http.MethodGet, "/snippet/create", app.snippetCreate)
router.HandlerFunc(http.MethodPost, "/snippet/create", app.snippetCreatePost)
// Create the middleware chain as normal.
standard := alice.New(app.recoverPanic, app.logRequest, secureHeaders)
// Wrap the router with the middleware and return it as normal.
return standard.Then(router)
}
Особенности передачи параметров в пакете httprouter
//данные передаются в контексте типа Params
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
params := httprouter.ParamsFromContext(r.Context()) //получаем данные запроса
id, err := strconv.Atoi(params.ByName("id")) //считываем по ключу id значение переданного id
if err != nil || id < 1 {
app.notFound(w)
return
}
//далее продолжаем обработку
поменяли обработку запроса в шаблоне
<td><a href='/snippet/view/{{.ID}}'>{{.Title}}</a></td>
Обработка ошибок
в пакете есть специальный метод app.notFound()
настройка в начале файла routes.go
func (app *application) routes() http.Handler {
router := httprouter.New()
router.NotFound = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
app.notFound(w)
})
позволит несуществующие пути обрабатывать ошибкой 404
Обработка форм
- Выполнить запрос GET к серверу для запроса формы
- Заполнение формы и отправка на сервер результата методом POST
- Обработка формы и валидация handler-ом
snippetCreatePost
- После проверки и добавление редирект на
/snippet/view/:id
для просмотра результата
Шаблон формы create.tmpl
{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
<div>
<label>Title:</label>
<input type='text' name='title'>
</div>
<div>
<label>Content:</label>
<textarea name='content'></textarea>
</div>
<div>
<label>Delete in:</label>
<input type='radio' name='expires' value='365' checked> One Year
<input type='radio' name='expires' value='7'> One Week
<input type='radio' name='expires' value='1'> One Day
</div>
<div>
<input type='submit' value='Publish snippet'>
</div>
</form>
{{end}}
Ссылка на вызов формы nav.html
в меню добавим пункт с вызовом формы
{{define "nav"}}
<nav>
<a href='/'>Home</a>
<!-- Add a link to the new form -->
<a href='/snippet/create'>Create snippet</a>
</nav>
{{end}}
Обработчик формы handler
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
app.render(w, http.StatusOK, "create.tmpl", data)
}
Парсинг формы
r.ParseForm()
метод парсинга и проверки формы. Парсит форму и сохраняет значения в map r.ParseForm
Для получения значений из полей формы, используем r.PostForm.Get()
метод. Например: r.PostForm.Get("title")
Функция обработчик формы:
func (app *application) snippetCreatePost(w http.ResponseWriter, r *http.Request) {
//парсинг формы и проверка ошибок
err := r.ParseForm()
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
//получение значений title и content
title := r.PostForm.Get("title")
content := r.PostForm.Get("content")
//преобразование в целое число, так как все значения приходят в формате string
expires, err := strconv.Atoi(r.PostForm.Get("expires"))
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
//выполнение запроса к базе данных
id, err := app.snippets.Insert(title, content, expires)
if err != nil {
app.serverError(w, err)
return
}
http.Redirect(w, r, fmt.Sprintf("/snippet/view/%d", id), http.StatusSeeOther)
}
Для справки
r.PostForm
map популярен только для POST
, PATCH
и PUT
запросов. Но еще есть карта r.Form
которая сохраняет не только значение тела запроса но и из строки /snippet/create?foo=bar
Стандартные методы r.FormValue()
и r.PostFormValue()
автор не рекомендует использовать из-за сложностей в обработке ошибок.
Обработка множественных значений
r.PostForm.Get() метод Get не работает с множественными значениями и вернет только первый
например флажки чекбокса
В этом случае вам нужно будет работать непосредственно с картой r.PostForm. Основным типом карты r.PostForm является url.Values, который, в свою очередь, имеет базовый тип map[string][]string
. Таким образом, для полей с несколькими значениями вы можете пройтись по базовой карте, чтобы получить к ним доступ следующим образом:
for i, item := range r.PostForm["items"] {
fmt.Fprintf(w, "%d: Item %s\n", i, item)
}
Ограничение размера формы
Если вы не отправляете составные данные (т.е. ваша форма имеет атрибут enctype="multipart/form-data"
), то тела запросов POST
, PUT
и PATCH
ограничены 10 МБ. Если это значение будет превышено, то r.ParseForm()
вернет ошибку.
Если вы хотите изменить это ограничение, вы можете использовать http.MaxBytesReader()
функцию.
// Limit the request body size to 4096 bytes
r.Body = http.MaxBytesReader(w, r.Body, 4096)
err := r.ParseForm()
if err != nil {
http.Error(w, "Bad Request", http.StatusBadRequest)
return
}
Кроме того, при достижении лимита MaxBytesReader устанавливает флаг на http.ResponseWriter
, который указывает серверу закрыть базовое TCP-соединение.
Валидация формы
// создадим переменную для валидации
fieldErrors := make(map[string]string)
// проверка, что title не пусто и не больше 100 знаков. Результат проблем записываем в нашу map с ключом именем поля
if strings.TrimSpace(title) == "" {
fieldErrors["title"] = "This field cannot be blank"
} else if utf8.RuneCountInString(title) > 100 {
fieldErrors["title"] = "This field cannot be more than 100 characters long"
}
// проверяем на пустоту content и записываем в map если есть проблемы
if strings.TrimSpace(content) == "" {
fieldErrors["content"] = "This field cannot be blank"
}
// проверяем что поле равно 1 7 или 365
if expires != 1 && expires != 7 && expires != 365 {
fieldErrors["expires"] = "This field must equal 1, 7 or 365"
}
// а теперь простая проверка всего map. Если map не пустой, значит есть ошибки и возвращаем ошибку. Точнее возвращаем весь map.
if len(fieldErrors) > 0 {
fmt.Fprint(w, fieldErrors)
return
}
Отображение ошибок на форме
- В обработчик шаблонов templates.go добавим поле Form
type templateData struct {
CurrentYear int
Snippet *models.Snippet
Snippets []*models.Snippet
Form any
}
поле Form необходимо для отправки результатов валидации формы
- В handlers.go добавим map с результатами валидации в структуру snippetCreateForm
type snippetCreateForm struct {
Title string
Content string
Expires int
FieldErrors map[string]string
}
- Заполним структуру значениями
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
FieldErrors: map[string]string{},
}
- Выполним валидацию
- Если есть ошибки то возвращаем ответ с ошибками
if len(form.FieldErrors) > 0 {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
return
}
т.е. вызываем эту же форму, только с заполненным map с ошибками, но если ошибок нет, то этот шаг не выполнится и пойдет обращение к базе данных
Изменяем шаблон для отображения ошибок
- Обновим create.tmpl шаблон формы
- Для повторного отображения введенных значений, используем значения поля Form
{{.Form.Title}}
и{{.Form.Content}}
- Для отображения ошибок валидации используем значения map:
{{.Form.FieldErrors.title}}
и другие - Шаблон стал теперь таким
{{define "title"}}Create a New Snippet{{end}}
{{define "main"}}
<form action='/snippet/create' method='POST'>
<div>
<label>Title:</label>
<!-- Use the `with` action to render the value of .Form.FieldErrors.title
if it is not empty. -->
{{with .Form.FieldErrors.title}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Re-populate the title data by setting the `value` attribute. -->
<input type='text' name='title' value='{{.Form.Title}}'>
</div>
<div>
<label>Content:</label>
<!-- Likewise render the value of .Form.FieldErrors.content if it is not
empty. -->
{{with .Form.FieldErrors.content}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Re-populate the content data as the inner HTML of the textarea. -->
<textarea name='content'>{{.Form.Content}}</textarea>
</div>
<div>
<label>Delete in:</label>
<!-- And render the value of .Form.FieldErrors.expires if it is not empty. -->
{{with .Form.FieldErrors.expires}}
<label class='error'>{{.}}</label>
{{end}}
<!-- Here we use the `if` action to check if the value of the re-populated
expires field equals 365. If it does, then we render the `checked`
attribute so that the radio input is re-selected. -->
<input type='radio' name='expires' value='365' {{if (eq .Form.Expires 365)}}checked{{end}}> One Year
<!-- And we do the same for the other possible values too... -->
<input type='radio' name='expires' value='7' {{if (eq .Form.Expires 7)}}checked{{end}}> One Week
<input type='radio' name='expires' value='1' {{if (eq .Form.Expires 1)}}checked{{end}}> One Day
</div>
<div>
<input type='submit' value='Publish snippet'>
</div>
</form>
{{end}}
- В обработчик handlers.go нужно обязательно добавить инициализацию Form для пустых значений, чтобы не было ошибки
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
data := app.newTemplateData(r)
data.Form = snippetCreateForm{
Expires: 365,
}
app.render(w, http.StatusOK, "create.tmpl", data)
}
Создание хелперов валидации
Просто выносим всю валидацию из кода в отдельный модуль internal/validator
mkdir internal/validator
touch internal/validator/validator.go
создаем файл validator.go
File: internal/validator/validator.go
package validator
import (
"strings"
"unicode/utf8"
)
// Define a new Validator type which contains a map of validation errors for our
// form fields.
type Validator struct {
FieldErrors map[string]string
}
// Valid() returns true if the FieldErrors map doesn't contain any entries.
func (v *Validator) Valid() bool {
return len(v.FieldErrors) == 0
}
// AddFieldError() adds an error message to the FieldErrors map (so long as no
// entry already exists for the given key).
func (v *Validator) AddFieldError(key, message string) {
// Note: We need to initialize the map first, if it isn't already
// initialized.
if v.FieldErrors == nil {
v.FieldErrors = make(map[string]string)
}
if _, exists := v.FieldErrors[key]; !exists {
v.FieldErrors[key] = message
}
}
// CheckField() adds an error message to the FieldErrors map only if a
// validation check is not 'ok'.
func (v *Validator) CheckField(ok bool, key, message string) {
if !ok {
v.AddFieldError(key, message)
}
}
// NotBlank() returns true if a value is not an empty string.
func NotBlank(value string) bool {
return strings.TrimSpace(value) != ""
}
// MaxChars() returns true if a value contains no more than n characters.
func MaxChars(value string, n int) bool {
return utf8.RuneCountInString(value) <= n
}
// PermittedInt() returns true if a value is in a list of permitted integers.
func PermittedInt(value int, permittedValues ...int) bool {
for i := range permittedValues {
if value == permittedValues[i] {
return true
}
}
return false
}
Корректируем handlers.go
для использования валидатора внесем изменения
добавим импорт:
"snippetbox.alexedwards.net/internal/validator" // New import
- Убрали map и поставили validator.Validator
type snippetCreateForm struct {
Title string
Content string
Expires int
validator.Validator
}
основное преимущество validator.Validator в том, что он содержит все поля map и методы обработки
- При заполнении структуры Form, теперь не нужно заполонять map с ошибками
form := snippetCreateForm{
Title: r.PostForm.Get("title"),
Content: r.PostForm.Get("content"),
Expires: expires,
// Remove the FieldErrors assignment from here.
}
- Вызов проверок валидации теперь доступны прямо на переменной form
form.CheckField(validator.NotBlank(form.Title), "title", "This field cannot be blank")
form.CheckField(validator.MaxChars(form.Title, 100), "title", "This field cannot be more than 100 characters long")
form.CheckField(validator.NotBlank(form.Content), "content", "This field cannot be blank")
form.CheckField(validator.PermittedInt(form.Expires, 1, 7, 365), "expires", "This field must equal 1, 7 or 365")
- Проверяем валидность формы
if !form.Valid() {
data := app.newTemplateData(r)
data.Form = form
app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
return
}
Автоматический парсинг форм
Автор предлагает использовать сторонний пакет go-playground/form
который по сути делает тоже самое, но более гибко и универсально