Перейти к основному содержимому

Веб на стандартной библиотеке Go

Разработчику

Это практическая точка входа в серверную разработку на Go без фреймворков. После этого материала проще осознанно выбрать Gin, Echo или Fiber, потому что базовая механика HTTP уже будет понятна на уровне стандартной библиотеки.

См. также: Фреймворки и библиотеки Go · Пример микросервиса · Gin · Важные интерфейсы.


Зачем stdlib, если есть Gin и Echo

Пакет net/http — полноценный HTTP-стек — сервер, клиент, TLS, cookies, multipart. Фреймворки строятся поверх него (кроме Fiber на fasthttp). Умение писать на stdlib нужно, чтобы:

  • понимать, что делают middleware и контекст во фреймворках;
  • собирать лёгкие сервисы без лишних зависимостей;
  • встраивать сторонние http.Handler в существующий сервер.

Маршрутизация

Классический ServeMux

mux := http.NewServeMux()
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok"))
})
mux.HandleFunc("GET /users/{id}", userByID)

log.Fatal(http.ListenAndServe(":8080", mux))

Разбор:

  • http.NewServeMux() создаёт маршрутизатор из стандартной библиотеки без сторонних зависимостей.
  • HandleFunc("GET /health", ...) связывает HTTP-метод и путь с функцией-обработчиком.
  • w.WriteHeader(http.StatusOK) явно выставляет код 200, затем w.Write(...) отправляет тело ответа.
  • mux.HandleFunc("GET /users/{id}", userByID) показывает path-паттерн с параметром маршрута.
  • http.ListenAndServe(":8080", mux) запускает сервер и передаёт ему роутер как главный обработчик.
  • log.Fatal(...) аварийно завершает процесс, если запуск сервера возвращает ошибку.

С Go 1.22+ ServeMux понимает метод HTTP и шаблоны вроде {id} в пути. Параметры читают через r.PathValue("id").


Обработчик как http.Handler

type greetHandler struct{ prefix string }

func (h greetHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "%s, %s", h.prefix, r.URL.Query().Get("name"))
}

mux.Handle("GET /hi", greetHandler{prefix: "Привет"})

Разбор:

  • Объявлен тип greetHandler, который хранит зависимость prefix в поле структуры.
  • Метод ServeHTTP(w, r) делает тип полноценной реализацией интерфейса http.Handler.
  • Внутри метода r.URL.Query().Get("name") читает query-параметр name.
  • fmt.Fprintf(...) формирует строку ответа напрямую в ResponseWriter.
  • mux.Handle(...) регистрирует экземпляр структуры как handler для заданного маршрута.

Тип с методом ServeHTTP удобен, когда обработчик хранит зависимости (БД, конфиг).


Query, формы и multipart

ИсточникКак читать
Query string ?a=1r.URL.Query().Get("a")
application/x-www-form-urlencodedr.ParseForm()r.FormValue("email")
multipart/form-datar.ParseMultipartForm(maxMem)r.FormValue, r.FormFile
func contact(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
email := r.FormValue("email")
// ...
}

Разбор:

  • Функция принимает стандартные интерфейсы записи ответа и чтения запроса.
  • Проверка r.Method != http.MethodPost ограничивает endpoint конкретным HTTP-методом.
  • http.Error(...) сразу отправляет код ошибки и текст клиенту, упрощая early-return сценарии.
  • r.ParseForm() парсит тело формы и query-параметры в структуру запроса.
  • r.FormValue("email") извлекает значение поля формы по ключу после успешного парсинга.
  • Такой шаблон подходит для HTML-форм и простых webhook-приёмников.

Для загрузки файлов — r.FormFile("avatar"), не забывать defer file.Close().


Middleware

Идиома — обёртка над http.Handler:

func withLogging(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}

mux.Handle("GET /api/", withLogging(apiHandler))

Разбор:

  • withLogging реализует middleware-паттерн: принимает next и возвращает новый http.Handler.
  • http.HandlerFunc адаптирует обычную функцию под интерфейс http.Handler.
  • start := time.Now() фиксирует время начала запроса для замера длительности.
  • next.ServeHTTP(w, r) передаёт управление следующему обработчику в цепочке.
  • После выполнения next пишется лог с методом, путём и временем обработки.
  • Оборачивание apiHandler через withLogging подключает middleware точечно к нужному роуту.

Цепочку можно собирать вручную или маленькой функцией chain(mw ...Middleware) http.Handler. Панику в production лучше перехватывать отдельным middleware и логировать, не полагаясь на "аварийный" ответ клиенту.


HTML-шаблоны

Пакет html/template (не text/template для веб-страниц) экранирует вывод и снижает риск XSS:

Код ITЗагрузка примера кода…

Разбор:

  • template.New("home").Parse(...) компилирует HTML-шаблон из строкового литерала.
  • template.Must(...) прерывает запуск приложения при ошибке шаблона, что полезно для fail-fast старта.
  • В handler устанавливается Content-Type: text/html; charset=utf-8, чтобы браузер корректно интерпретировал ответ.
  • page.Execute(w, data) подставляет значения структуры в шаблонные выражения {{...}}.
  • r.URL.Query().Get("name") позволяет динамически отображать значение из URL.
  • Пакет html/template автоматически экранирует небезопасные вставки и снижает риск XSS.

Шаблоны компилируют один раз (template.Must при старте). Для JSON-API шаблоны не нужны — достаточно encoding/json (важные интерфейсы).


JSON-ответы

func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
enc := json.NewEncoder(w)
enc.SetIndent("", " ")
_ = enc.Encode(v)
}

Разбор:

  • Утилита writeJSON централизует отправку JSON и убирает дублирование кода по handlers.
  • w.Header().Set("Content-Type", "application/json") фиксирует MIME-тип ответа.
  • w.WriteHeader(status) устанавливает HTTP-статус до записи тела.
  • json.NewEncoder(w) пишет JSON прямо в поток ответа без промежуточной строки.
  • enc.SetIndent("", " ") включает читаемое форматирование, удобное в учебных примерах и ручной проверке.
  • Аргумент v any делает функцию универсальной для разных структур ответа.

Ошибки API обычно оформляют единой структурой { "error": "..." } и фиксированными кодами HTTP.

Дополнительно стоит сериализовать время, идентификаторы и nullable-поля в стабильном формате, чтобы фронтенд не зависел от случайных деталей реализации.


Контекст запроса

r.Context() наследует отмену при обрыве клиента. Для таймаутов и отмены БД:

ctx, cancel := context.WithTimeout(r.Context(), 3*time.Second)
defer cancel()
row := db.QueryRowContext(ctx, "SELECT ...", id)

Разбор:

  • r.Context() несёт сигнал отмены запроса и дедлайн, связанный с жизненным циклом клиента.
  • context.WithTimeout(..., 3*time.Second) добавляет ограничение времени на внутреннюю операцию.
  • defer cancel() освобождает ресурсы контекста сразу после завершения работы handler.
  • db.QueryRowContext(...) передаёт контекст в драйвер БД, чтобы запрос остановился при таймауте или отмене.
  • Такой подход защищает сервис от зависших внешних вызовов и накопления заблокированных горутин.

Подробнее о context.Context — в важных интерфейсах. Фреймворки прокидывают тот же контекст в свой API.


Минимальная структура проекта

Для учебного API удобно сразу придерживаться простой структуры:

  • cmd/api/main.go — запуск сервера и wiring зависимостей;
  • internal/http/handlers — HTTP-обработчики;
  • internal/service — бизнес-логика;
  • internal/repo — доступ к данным.

Такой каркас уменьшает скомканность кода по мере роста проекта.


Корректная остановка сервера

ListenAndServe блокирует навсегда. Для деплоя в Kubernetes нужен graceful shutdown:

Код ITЗагрузка примера кода…

Разбор:

  • http.Server создаётся отдельно, чтобы управлять жизненным циклом сервера более гибко.
  • Сервер стартует в горутине, а основной поток слушает системные сигналы завершения.
  • signal.Notify подписывает канал на SIGINT и SIGTERM, типичные сигналы остановки процесса.
  • После сигнала формируется timeout-контекст для ограниченного по времени graceful shutdown.
  • srv.Shutdown(ctx) прекращает приём новых соединений и ожидает завершения активных запросов.
  • Логирование ошибки shutdown помогает диагностировать долгие или зависшие обработчики.

Shutdown перестаёт принимать новые соединения и ждёт завершения активных запросов в пределах таймаута.


Метрики Prometheus

Типичный стек observability — endpoint /metrics для Prometheus и визуализация в Grafana. Минимальная интеграция через prometheus/client_golang:

Код ITЗагрузка примера кода…

Разбор:

  • promauto регистрирует метрику в default registry при init пакета.
  • CounterVec с labels method, path, status даёт разбивку RPS и кодов ответа.
  • promhttp.Handler() отдаёт текстовый exposition format на /metrics.
  • Middleware оборачивает handler и считает статус через обёртку над ResponseWriter.

Grafana подключают Prometheus как datasource и строят dashboard (rate, latency histogram, error ratio). Для latency добавляют Histogram или Summary и middleware с time.Since.

runtime/metrics

С Go 1.16+ пакет runtime/metrics читает внутренние метрики runtime (GC, heap). Их можно комбинировать с Prometheus для полной картины сервиса.

Подробнее про observability в инфраструктуре — экосистема Prometheus/Kubernetes в популярных проектах.


Когда переходить на фреймворк

ЗадачаStdlib достаточно
Healthcheck, webhook, внутренний APIЧасто да
Группы маршрутов, валидация, OpenAPIУдобнее Gin/Echo
Максимальный RPS на одном ядреСмотреть бенчмарки; иногда Fiber

Практика: Gin · Echo · Простые приложения.

Следующий шаг после этой статьи: собрать такой же endpoint в Gin и Fiber, затем сравнить читаемость кода и набор middleware.


Дополнительные сниппеты с разбором

Пример — чтение path-параметра в Go 1.22+

func userByID(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
if id == "" {
http.Error(w, "id is required", http.StatusBadRequest)
return
}
writeJSON(w, http.StatusOK, map[string]string{"id": id})
}

Разбор:

  • r.PathValue("id") получает значение параметра из маршрута вида "/users/{id}".
  • Проверка id == "" добавляет защиту от некорректного вызова handler вне ожидаемого маршрута.
  • http.Error(...) отправляет ошибку с кодом 400 и завершает выполнение через return.
  • writeJSON(...) переиспользует общую функцию сериализации и сохраняет единый формат ответа.
  • Такой handler демонстрирует чистую связку — валидация входа, ранний выход при ошибке, успешный JSON-ответ.

Пример — request timeout middleware на stdlib

func withTimeout(next http.Handler, d time.Duration) http.Handler {
return http.TimeoutHandler(next, d, `{"error":"timeout"}`)
}

mux.Handle("GET /slow", withTimeout(slowHandler, 2*time.Second))

Разбор:

  • http.TimeoutHandler оборачивает обычный handler и завершает запрос по дедлайну.
  • Аргумент d задаёт максимальное время обработки одного запроса.
  • Третий параметр задаёт тело ответа при таймауте, чтобы клиент получил читаемую причину.
  • Оборачивание маршрута точечно позволяет применять разные лимиты к разным endpoint.
  • Такой middleware полезен для внешних API, где важно не держать соединения бесконечно.

Пример — декодирование JSON-запроса

Код ITЗагрузка примера кода…

Разбор:

  • json.NewDecoder(r.Body).Decode(&body) читает и десериализует JSON прямо из тела запроса.
  • Анонимная структура с тегом json:"name" задаёт минимальный контракт входных данных.
  • TrimSpace убирает пробелы по краям и предотвращает "пустые" значения вроде " ".
  • При ошибках форматирования или валидации handler возвращает 400 с понятной причиной.
  • writeJSON отправляет подтверждение создания с кодом 201, сохраняя единый стиль ответов.

Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.