Веб на стандартной библиотеке 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=1 | r.URL.Query().Get("a") |
application/x-www-form-urlencoded | r.ParseForm() → r.FormValue("email") |
multipart/form-data | r.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с labelsmethod,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.
С 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 как основа веб-интеграций.