Alex Edwards Let's Go часть 1

edwardsLetsGo учебный материал по написанию приложения на Go с авторизацией пользователя. Краткий конспект с основными мыслями (главы с 1 по 8)

Основы 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) 
}

Типы адресов в обработчике

  1. /snippet/view — фиксированный адрес
  2. /snippet/view/ — не фиксированный адрес и под ним обрабатывается поддерево
  3. Для корня проверка если указано что-то отличающееся от /
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
    }

Структура проекта

  1. Main оставить в одном файле
  2. 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)
    }

Шаблоны отдельно

Делаем составные шаблоны

  1. Базовый шаблон 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):

  1. func (*ServeMux) Handle – это метод структуры ServeMux.
  2. 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/").

🔹 Когда используется?

  1. Когда нужно вручную определить обработчик для запроса (например, для кастомной логики маршрутизации).
  2. В middleware, чтобы проверить, какой обработчик будет вызван для запроса.
  3. Для логирования или отладки (чтобы узнать, какой 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"
}

Вывод:

$$Handler: true 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 / в логах появится:

$$Request: GET / → Handler: http.HandlerFunc, Pattern: /$$

🔹 Как работает внутри?

  1. Проверяет Host (если задан в ServeMux).
  2. Ищет точное совпадение пути (например, /users).
  3. Ищет самое длинное совпадение по префиксу (если путь зарегистрирован с /users/, то запрос /users/123 тоже сработает).
  4. Если ничего не найдено – возвращает 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. Она отвечает за:

  1. Поиск подходящего обработчика (на основе URL пути)
  2. Вызов этого обработчика (или возврат 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]

Пошаговый алгоритм:

  1. Проверяет Host (если в ServeMux есть пути с указанием хоста, например "example.com").
  2. Ищет точное совпадение пути (например, /users).
  3. Ищет самое длинное совпадение по префиксу (если путь зарегистрирован как /users/, то /users/123 тоже сработает).
  4. Если обработчик не найден — вызывает 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"))
}

🛠 Что происходит при запросе?

  1. Запрос GET /

    • ServeHTTP находит точное совпадение с / → вызывает homeHandler.
  2. Запрос GET /about

    • Совпадение с /about → вызывает aboutHandler.
  3. Запрос 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)
}

🔥 Ключевые особенности

  1. Иерархия путей:

    • /images/ совпадёт с /images/logo.png.
    • /images (без слеша) — только с точным совпадением.
  2. Host-specific пути:

    mux.Handle("example.com/", handler) // Сработает только для example.com
    
  3. Дефолтный обработчик 404:
    Можно заменить через mux.NotFound = customHandler.


🎯 Вывод

  • ServeHTTPядро роутинга в net/http.
  • Автоматически обрабатывает префиксы, хосты и 404.
  • Может быть переопределён для кастомной логики.

Что нужно сделать,чтобы получить Handle

  1. Создать структуру type home struct {} - или любой объект
  2. Создать функцию обработчик с вызовом ServeHTTP
func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) { w.Write([]byte("This is my home page")) }
  1. Используем в коде
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()

Логирование

  1. Стандартный лог выводит в терминал
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

Логирование работы сервера

  1. Создать структуру для параметров сервера 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"
)

Для работы динамических шаблонов нужно:

  1. Создать шаблоны
  2. Выстроить иерархию внутри них (каждый блок и раздел лучше делать в отдельном шаблоне и соединять в один, например base)
  3. Создать массив строк с наименованием шаблонов и путей к ним
files := []string{ 
	"./ui/html/base.tmpl", 
	"./ui/html/partials/nav.tmpl", 
	"./ui/html/pages/view.tmpl", 
}
  1. Пропарсить шаблоны
ts, err := template.ParseFiles(files...) 
if err != nil {
	app.serverError(w, err) 
	return 
}
  1. Собрать 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 
}

Эта структура будет работать с базой данных и шаблонами

  1. . — весь контекст переданный в этот блок
  2. Передать структуру в шаблон: err = ts.ExecuteTemplate(w, "base", snippet)
  3. Использовать поля структуры в шаблоне {{.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}}
  1. Создать структуру
type templateData struct { 
	Snippet *models.Snippet 
}
  1. Выполнить все действия, описанные выше по парсингу шаблонов
  2. Создать экземпляр структуры данных
data := &templateData{ 
	Snippet: snippet, 
}
  1. Передать данные в генератор html кода
err = ts.ExecuteTemplate(w, "base", data) 
	if err != nil { 
		app.serverError(w, err) 
	}
  1. В шаблоне значения полей тогда будут вызываться с указанием полного пути поля: {{.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

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
}

Как избежать ошибок при рендеринге динамического кода

  1. Делать рендер кода
  2. Записывать результат в буфер
  3. Проверять на отсутствие ошибок
  4. делать выдачу в 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)
}

Динамические данные в шаблонах

  1. Создадим специальную структуру для передачи динамических данных в шаблон
type templateData struct {
    CurrentYear int
    Snippet     *models.Snippet
    Snippets    []*models.Snippet
}
  1. Создадим функцию helpers для возврата указателя на структуру с данными и расчетом динамических данных
func (app *application) newTemplateData(r *http.Request) *templateData {//получаем на вход указатель с данными запроса 
    return &templateData{ //и возвращаем указатель с данными ответа
        CurrentYear: time.Now().Year(),// по дороге посчитаем какой текущий год
    }
}
  1. Делаем вызов функции 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)
  1. Повторяем такие вызовы для всех обработчиков
  2. В шаблонах обрабатываем данные из структуры templateData как {{.Snippets}} или {{.Snippet}} и текущий год как {{.CurrentYear}}

Пользовательские функции в шаблонах

  1. Создаем функцию, которую планируем добавить в шаблон
func humanDate(t time.Time) string {
    return t.Format("02 Jan 2006 at 15:04")
}
  1. Создаем объект template.FuncMap и в нем объявляем свою функцию (например humanDate()). Фактически в переменной function создается карта: ключ - значение, где значение, это название функции, которое мы передаем в шаблон.
var functions = template.FuncMap{
    "humanDate": humanDate,
}
  1. Метод должен быть зарегистрирован до парсинга шаблона
    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
    }
  1. Используем функцию в шаблоне как и другие функции
<td>{{humanDate .Created}}</td>
или
<time>Created: {{.Created | humanDate}}</time>
или
<time>{{.Created | humanDate | printf "Created: %s"}}</time>

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)
    })
}

Создадим обработчик заголовков для всех страниц

Будем возвращать шаблоны 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
  1. Создадим функцию для создания заголовков
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)
    })
}
  1. Создадим функцию обработчик маршрутов
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)
}
  1. После обработки маршрута для каждой страницы будут создан заголовок

Функция логирования запросов

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) // Обработка ошибки
    }
}

Что происходит:

  1. Если вызвана panic("oops! something went wrong"), recover() вернёт строку "oops! something went wrong".
  2. fmt.Errorf() преобразует это значение в error.
  3. Ошибка передаётся в метод app.serverError() для логирования/отправки клиенту.

Зачем это нужно:

  • Чтобы избежать аварийного завершения программы при панике.
  • Логировать ошибки и отправлять клиенту понятный ответ (например, 500 Internal Server Error).

Важно:

  • recover() работает только внутри defer.
  • Всегда проверяйте, что recover() вернул не nil.

Заключительный вариант вызова middleware

    return app.recoverPanic(app.logRequest(secureHeaders(mux)))

Делается обработка для всех запускаемых горутин.

Автор рекомендует пакет justinas/alice

Для удобного и читабельного управления middleware можно использовать пакет justinas/alice

Особенности:

  1. вместо return myMiddleware1(myMiddleware2(myMiddleware3(myHandler))) будем писать return alice.New(myMiddleware1, myMiddleware2, myMiddleware3).Then(myHandler)
  2. есть методы 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.

сделав анализ современного развития этих пакетов можно прийти к выводу:

Чистые 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

Обработка форм

  1. Выполнить запрос GET к серверу для запроса формы
  2. Заполнение формы и отправка на сервер результата методом POST
  3. Обработка формы и валидация handler-ом snippetCreatePost
  4. После проверки и добавление редирект на /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.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
    }

Отображение ошибок на форме

  1. В обработчик шаблонов templates.go добавим поле Form
type templateData struct {
    CurrentYear int
    Snippet     *models.Snippet
    Snippets    []*models.Snippet
    Form        any
}

поле Form необходимо для отправки результатов валидации формы

  1. В handlers.go добавим map с результатами валидации в структуру snippetCreateForm
type snippetCreateForm struct {
    Title       string
    Content     string
    Expires     int
    FieldErrors map[string]string
}
  1. Заполним структуру значениями
    form := snippetCreateForm{
        Title:       r.PostForm.Get("title"),
        Content:     r.PostForm.Get("content"),
        Expires:     expires,
        FieldErrors: map[string]string{},
    }
  1. Выполним валидацию
  2. Если есть ошибки то возвращаем ответ с ошибками
    if len(form.FieldErrors) > 0 {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

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

Изменяем шаблон для отображения ошибок

  1. Обновим create.tmpl шаблон формы
  2. Для повторного отображения введенных значений, используем значения поля Form {{.Form.Title}} и {{.Form.Content}}
  3. Для отображения ошибок валидации используем значения map: {{.Form.FieldErrors.title}} и другие
  4. Шаблон стал теперь таким
{{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}}
  1. В обработчик 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
  1. Убрали map и поставили validator.Validator
type snippetCreateForm struct {
    Title               string 
    Content             string 
    Expires             int    
    validator.Validator
}

основное преимущество validator.Validator в том, что он содержит все поля map и методы обработки

  1. При заполнении структуры Form, теперь не нужно заполонять map с ошибками
    form := snippetCreateForm{
        Title:   r.PostForm.Get("title"),
        Content: r.PostForm.Get("content"),
        Expires: expires,
        // Remove the FieldErrors assignment from here.
    }
  1. Вызов проверок валидации теперь доступны прямо на переменной 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")
  1. Проверяем валидность формы
    if !form.Valid() {
        data := app.newTemplateData(r)
        data.Form = form
        app.render(w, http.StatusUnprocessableEntity, "create.tmpl", data)
        return
    }

Автоматический парсинг форм

Автор предлагает использовать сторонний пакет go-playground/form который по сути делает тоже самое, но более гибко и универсально