Пакет net/http в Go

net/http — один из лучших примеров того, как стандартная библиотека Go закрывает большинство потребностей без внешних зависимостей. На нём построены тысячи production-сервисов, а популярные фреймворки вроде Gin и Echo — это просто тонкие обёртки поверх него. Понять net/http изнутри значит понять как работает любой Go веб-фреймворк.


Как устроен HTTP-сервер в Go

В основе всего один интерфейс:

go
type Handler interface { ServeHTTP(ResponseWriter, *Request) }

Весь net/http построен вокруг этого интерфейса. Любой тип, реализующий ServeHTTP — это HTTP-обработчик. Сервер принимает соединение, парсит запрос и вызывает ServeHTTP нужного обработчика.

go
// Минимальный рабочий сервер 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 позволяет использовать функцию напрямую:

go
// 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-паттерны с обработчиками:

go
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 маршрутизатор значительно улучшили:

go
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 — входящий запрос

go
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 — исходящий ответ

go
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. Удобно вынести в хелперы:

go
// Декодирование запроса 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:

go
type Middleware func(http.Handler) http.Handler

Логирование запросов

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

Аутентификация

go
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

go
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

go
// Применяем 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-запросов:

go
// Никогда не используйте 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-сервер должен корректно завершаться: дождаться активных запросов и только потом остановиться. Иначе клиенты получат обрыв соединения:

go
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:

go
// Вот что делает 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. Переходим?