Пакет net/http в Go
net/http — один из лучших примеров того, как стандартная библиотека Go закрывает большинство потребностей без внешних зависимостей. На нём построены тысячи production-сервисов, а популярные фреймворки вроде Gin и Echo — это просто тонкие обёртки поверх него. Понять net/http изнутри значит понять как работает любой Go веб-фреймворк.
Как устроен HTTP-сервер в Go
В основе всего один интерфейс:
type Handler interface {
ServeHTTP(ResponseWriter, *Request)
}
Весь net/http построен вокруг этого интерфейса. Любой тип, реализующий ServeHTTP — это HTTP-обработчик. Сервер принимает соединение, парсит запрос и вызывает ServeHTTP нужного обработчика.
// Минимальный рабочий сервер
package main
import (
"fmt"
"net/http"
)
type HelloHandler struct{}
func (h HelloHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello, World!")
}
func main() {
handler := HelloHandler{}
http.ListenAndServe(":8080", handler)
}
HandlerFunc — функция как обработчик
Реализовывать интерфейс для каждого обработчика — многословно. http.HandlerFunc позволяет использовать функцию напрямую:
// http.HandlerFunc — это просто тип-адаптер
type HandlerFunc func(ResponseWriter, *Request)
func (f HandlerFunc) ServeHTTP(w ResponseWriter, r *Request) {
f(w, r)
}
// Использование
func helloHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "Hello!")
}
http.ListenAndServe(":8080", http.HandlerFunc(helloHandler))
ServeMux — маршрутизатор
http.ServeMux — стандартный маршрутизатор. Сопоставляет URL-паттерны с обработчиками:
mux := http.NewServeMux()
mux.HandleFunc("/", homeHandler)
mux.HandleFunc("/users", usersHandler)
mux.HandleFunc("/users/", userHandler) // trailing slash — ловит всё начинающееся с /users/
http.ListenAndServe(":8080", mux)
Улучшенный маршрутизатор в Go 1.22
До Go 1.22 стандартный ServeMux был ограничен — нельзя было указать HTTP-метод или path параметры (/users/{id}). Это главная причина популярности сторонних роутеров. В Go 1.22 маршрутизатор значительно улучшили:
mux := http.NewServeMux()
// Метод + путь
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
// Path параметры через {name}
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
// Получение path параметра
func getUser(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // Go 1.22+
fmt.Fprintf(w, "user id: %s", id)
}
Стандартный маршрутизатор Go 1.22 закрывает большинство базовых потребностей. Для сложных случаев (middleware chains, группы роутов, валидация) всё ещё удобнее фреймворки.
ResponseWriter и Request
http.Request — входящий запрос
func handler(w http.ResponseWriter, r *http.Request) {
// Метод и URL
fmt.Println(r.Method) // "GET", "POST", ...
fmt.Println(r.URL.Path) // "/users/42"
fmt.Println(r.URL.Query()) // query параметры: ?page=1&limit=10
// Query параметры
page := r.URL.Query().Get("page")
limit := r.URL.Query().Get("limit")
// Заголовки
token := r.Header.Get("Authorization")
contentType := r.Header.Get("Content-Type")
// Тело запроса
body, err := io.ReadAll(r.Body)
defer r.Body.Close() // всегда закрывать!
// Контекст запроса
ctx := r.Context()
userID := ctx.Value(userIDKey)
}
http.ResponseWriter — исходящий ответ
func handler(w http.ResponseWriter, r *http.Request) {
// Заголовки нужно писать ДО WriteHeader и Write
w.Header().Set("Content-Type", "application/json")
w.Header().Set("X-Request-ID", "abc-123")
// Статус код — тоже до Write
w.WriteHeader(http.StatusCreated) // 201
// Тело ответа
w.Write([]byte(`{"id": 1}`))
}
Порядок важен: Header().Set() → WriteHeader() → Write(). Попытка установить заголовок после Write — молчаливо игнорируется, Go выведет предупреждение в лог.
Работа с JSON
В реальных API постоянно нужно читать и писать JSON. Удобно вынести в хелперы:
// Декодирование запроса
func decodeJSON(r *http.Request, dst interface{}) error {
defer r.Body.Close()
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields() // строгий режим
return decoder.Decode(dst)
}
// Отправка JSON-ответа
func writeJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Отправка ошибки
func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// Использование
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := decodeJSON(r, &req); err != nil {
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
user, err := createUser(r.Context(), req)
if err != nil {
writeError(w, http.StatusInternalServerError, "failed to create user")
return
}
writeJSON(w, http.StatusCreated, user)
}
Middleware
Middleware — обёртка вокруг обработчика, добавляющая поведение до или после его вызова. В Go middleware реализуется как функция, принимающая Handler и возвращающая Handler:
type Middleware func(http.Handler) http.Handler
Логирование запросов
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
// Оборачиваем ResponseWriter чтобы перехватить статус код
wrapped := &responseWriter{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(wrapped, r) // вызываем следующий обработчик
log.Printf(
"%s %s %d %v",
r.Method,
r.URL.Path,
wrapped.status,
time.Since(start),
)
})
}
// Обёртка для перехвата статус кода
type responseWriter struct {
http.ResponseWriter
status int
}
func (rw *responseWriter) WriteHeader(status int) {
rw.status = status
rw.ResponseWriter.WriteHeader(status)
}
Аутентификация
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token == "" {
writeError(w, http.StatusUnauthorized, "missing token")
return // не вызываем next
}
userID, err := validateToken(token)
if err != nil {
writeError(w, http.StatusUnauthorized, "invalid token")
return
}
// Передаём userID в контекст
ctx := context.WithValue(r.Context(), userIDKey, userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
CORS
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
Цепочка middleware
// Применяем middleware справа налево — первый в списке выполняется первым
func chain(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
// Использование
mux := http.NewServeMux()
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
handler := chain(mux,
loggingMiddleware, // выполняется первым
corsMiddleware, // вторым
authMiddleware, // третьим
)
http.ListenAndServe(":8080", handler)
http.Client — выполнение запросов
net/http — не только сервер, но и клиент для выполнения HTTP-запросов:
// Никогда не используйте http.DefaultClient в production!
// У него нет таймаутов — соединение может висеть вечно
client := &http.Client{
Timeout: 10 * time.Second, // общий таймаут запроса
Transport: &http.Transport{
MaxIdleConns: 100,
MaxIdleConnsPerHost: 10,
IdleConnTimeout: 90 * time.Second,
},
}
// GET запрос
func getUser(ctx context.Context, id int) (*User, error) {
url := fmt.Sprintf("https://api.example.com/users/%d", id)\n
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Authorization", "Bearer "+token)
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("do request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &user, nil
}
Graceful Shutdown
Production-сервер должен корректно завершаться: дождаться активных запросов и только потом остановиться. Иначе клиенты получат обрыв соединения:
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", healthHandler)
// ... другие маршруты
server := &http.Server{
Addr: ":8080",
Handler: mux,
ReadTimeout: 5 * time.Second, // время на чтение запроса
WriteTimeout: 10 * time.Second, // время на запись ответа
IdleTimeout: 120 * time.Second,// keep-alive соединения
}
// Запускаем сервер в горутине
go func() {
log.Println("server started on :8080")
if err := server.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("server error: %v", err)
}
}()
// Ждём сигнала завершения (Ctrl+C или kill)
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("shutting down server...")
// Даём 30 секунд на завершение активных запросов
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("forced shutdown: %v", err)
}
log.Println("server stopped")
}
net — одним абзацем
net/http построен поверх пакета net. Если нужно понять уровень абстракции — вот как выглядит TCP-сервер напрямую через net:
// Вот что делает net/http под капотом (упрощённо)
listener, err := net.Listen("tcp", ":8080")
for {
conn, err := listener.Accept() // принимаем соединение
go handleConn(conn) // обрабатываем в горутине
}
net/http добавляет поверх этого: парсинг HTTP-протокола, маршрутизацию, keep-alive соединения, TLS, и всё что мы разобрали выше. Напрямую с net работают когда нужен кастомный протокол поверх TCP — это уже за пределами большинства backend-задач.
Вопросы на собеседовании
Q: Что такое http.Handler? Как реализовать свой?
A: Интерфейс с одним методом ServeHTTP(ResponseWriter, *Request). Любой тип реализующий этот метод является HTTP-обработчиком. Удобная альтернатива — http.HandlerFunc: адаптер превращающий обычную функцию в Handler через приведение типа.
Q: Какой порядок вызовов в ResponseWriter и почему он важен?
A: Сначала Header().Set(), потом WriteHeader(), потом Write(). После первого вызова Write() заголовки и статус код уже отправлены клиенту — изменить их невозможно. Go выведет предупреждение в лог но не вернёт ошибку.
Q: Что такое middleware? Как реализовать цепочку?
A: Функция типа func(http.Handler) http.Handler — принимает обработчик и возвращает новый с дополнительным поведением. Цепочка строится последовательным оборачиванием: каждый middleware получает следующий как аргумент и вызывает его внутри своего ServeHTTP.
Q: Почему нельзя использовать http.DefaultClient в production?
A: У DefaultClient нет таймаутов — запрос к зависшему серверу будет висеть вечно, занимая горутину и соединение. В production всегда создают клиент с явными таймаутами и настроенным Transport.
Q: Что такое Graceful Shutdown и как реализовать?
A: Корректное завершение сервера: перестаём принимать новые соединения, ждём завершения активных запросов, потом останавливаемся. Реализуется через server.Shutdown(ctx) после получения OS-сигнала (SIGINT/SIGTERM). Без этого активные запросы получат обрыв соединения.
Q: Что нового в маршрутизаторе Go 1.22?
A: Поддержка HTTP-методов в паттерне (GET /users), path-параметры через фигурные скобки (/users/{id}), получение параметра через r.PathValue("id"). До 1.22 для этого требовались сторонние роутеры.
Q: Как передать данные из middleware в обработчик?
A: Через контекст запроса: r.WithContext(context.WithValue(r.Context(), key, value)). Обработчик читает через r.Context().Value(key). Ключ должен быть собственного типа чтобы избежать коллизий.
Q: Зачем закрывать r.Body?
A: r.Body — это сетевое соединение. Если не закрыть — соединение не вернётся в пул keep-alive, накопятся утечки файловых дескрипторов. defer r.Body.Close() сразу после входа в обработчик — обязательное правило.
Следующая статья — Gin и Echo: почему фреймворки, чем отличаются, когда что выбирать. А потом итоговый проект — Todo API. Переходим?