Разработка REST API с пакетом net/http
Введение
Пакет net/http — это сердце веб-разработки в Go. Он встроен в язык, невероятно быстрый, надёжный и даёт полный контроль над всем процессом. Многие популярные фреймворки (Gin, Echo, Fiber) построены поверх него, но в 2026 году всё больше разработчиков выбирают чистый net/http для микросервисов, API и даже полноценных приложений — потому что это меньше зависимостей, лучше производительность и проще отладка.
Мы пройдём путь от "Hello World" до реального REST API с middleware, graceful shutdown, обработкой JSON, файлов, ошибок и лучшими практиками. После него вы сможете самостоятельно писать свои веб-сервисы.
Инструменты для разработки REST API
Для тестирования ваших REST API мы рекомендуем использовать следующие инструменты:
- Postman — Стандартный выбор многих разработчиков для тестирования REST API
- ApiDog — Платформа для тестирования и документирования REST API. Имеет огромный функционал и интеграции с другими инструментами разработки. Предпочтительнее научиться работать с ним из-за его удобства и мощных возможностей.
- cURL — Простой и мощный инструмент для тестирования REST API
В данном уроке мы будем использовать стандартный cURL из-за его простоты, но рекомендуем научиться работать с ApiDog, чтобы получить лучший опыт разработки REST API.
Самый простой сервер — Hello World
package main
import (
"fmt"
"net/http"
)
func main() {
// Обработчик для корневого пути "/"
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Привет, мир! 🌍\n")
fmt.Fprintf(w, "Твой путь: %s\n", r.URL.Path)
fmt.Fprintf(w, "Метод запроса: %s\n", r.Method)
})
fmt.Println("Сервер запущен на http://localhost:8080")
fmt.Println("Нажми Ctrl+C для остановки")
// Запуск сервера на порту 8080
log.Fatal(http.ListenAndServe(":8080", nil))
}
Что тут происходит?
http.HandleFunc("/", handler)— когда приходит запрос на "/", вызывается эта функция.handlerимеет сигнатуруfunc(http.ResponseWriter, *http.Request).ResponseWriter— то, куда ты пишешь ответ (текст, JSON, заголовки).*http.Request— вся информация о запросе: путь, метод, заголовки, тело, параметры.ListenAndServeзапускает сервер и блокируетmain()(поэтому используемlog.Fatalдля обработки ошибок).
Для тестирования используйте curl:
curl -X GET http://localhost:8080
# Привет, мир! 🌍
# Твой путь: /
# Метод запроса: GET
HTTP-методы
Когда ты пишешь веб-сервис, браузер или клиент (Postman, curl) отправляет запросы с определённым методом — это слово в начале строки запроса (GET, POST и т.д.). Метод говорит серверу, что именно хочет сделать клиент.
В Go (net/http) ты всегда можешь узнать метод через r.Method и реагировать по-разному.
GET — "Получение данных"
Что делает:
Запрашивает ресурс (данные) с сервера. Не меняет ничего на сервере.
Аналогия:
Зашёл в библиотеку и попросил книгу почитать — книгу тебе дали, но она осталась в библиотеке.
Когда использовать:
- Получить список пользователей
/users - Получить одного пользователя
/users/42 - Поиск
/search?q=go - Главная страница сайта
Важные особенности:
- Безопасный (не меняет состояние)
- Идемпотентный (сколько ни вызывай — результат один и тот же)
- Можно кэшировать
- Параметры в URL (query string):
?name=Аня&age=25 - Не должен иметь тело запроса
Идемпоте́нтность («равносильность») — свойство объекта или операции при повторном применении операции к объекту давать тот же результат, что и при первом.
func getUserHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем, что запрос GET
if r.Method != http.MethodGet {
http.Error(w, "Только GET", http.StatusMethodNotAllowed)
return
}
// ... отдаём данные
}
// Или с Go 1.22+
mux := http.NewServeMux()
mux.HandleFunc("GET /users", getUser)
POST — "Создание ресурса"
Что делает:
Отправляет данные на сервер для создания нового ресурса.
Аналогия:
Принёс в библиотеку новую книгу и отдал библиотекарю — теперь она есть в каталоге.
Когда использовать:
- Регистрация пользователя
- Создание заказа
- Отправка формы
- Загрузка файла
Особенности:
- Меняет состояние сервера
- Не идемпотентный (два одинаковых POST — два разных ресурса)
- Данные в теле запроса (JSON, форма)
- Обычно возвращает статус 201 Created и новый объект
func createUserHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем, что запрос POST
if r.Method != http.MethodPost {
http.Error(w, "Только POST", http.StatusMethodNotAllowed)
return
}
// Читаем тело, создаём пользователя
w.WriteHeader(http.StatusCreated)
}
// Или с Go 1.22+
mux := http.NewServeMux()
mux.HandleFunc("POST /users", createUser)
PUT — "Замени ресурс полностью"
Что делает:
Полностью заменяет существующий ресурс новыми данными.
Аналогия:
Принёс в библиотеку новую версию книги и сказал: "Замените старую полностью этой".
Когда использовать:
- Обновление профиля пользователя (все поля)
- Замена всего объекта
Особенности:
- Идемпотентный (несколько одинаковых PUT — один и тот же результат)
- Если ресурса нет — может создать (но лучше использовать POST для создания)
- Все данные должны быть в теле
func updateUserHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем, что запрос PUT
if r.Method != http.MethodPut {
http.Error(w, "Только PUT", http.StatusMethodNotAllowed)
return
}
// Полная замена пользователя
}
// Или с Go 1.22+
mux := http.NewServeMux()
mux.HandleFunc("PUT /users", updateUserHandler)
PATCH — "Измени часть ресурса"
Что делает:
Обновляет только указанные поля ресурса.
Аналогия:
Пришёл в библиотеку и сказал: "В книге на странице 100 исправьте опечатку".
Когда использовать:
- Частичное обновление профиля (только email)
- Изменение статуса заказа
Особенности:
- Не обязательно идемпотентный (зависит от реализации)
- В теле — только изменяемые поля (часто JSON Patch или просто объект)
func patchUserHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем, что запрос PATCH
if r.Method != http.MethodPatch {
http.Error(w, "Только PATCH", http.StatusMethodNotAllowed)
return
}
// Обновляем только те поля, что пришли
}
// Или с Go 1.22+
mux := http.NewServeMux()
mux.HandleFunc("PATCH /users", patchUserHandler)
DELETE — "Удали ресурс"
Что делает:
Удаляет ресурс с сервера.
Аналогия:
Попросил библиотекаря убрать книгу с полки навсегда.
Когда использовать:
- Удаление пользователя
- Удаление заказа
- Очистка корзины
Особенности:
- Идемпотентный (удаляешь существующий — 200/204, удаляешь несуществующий — тоже 204)
- Обычно без тела
- Возвращает 204 No Content (если успешно)
func deleteUserHandler(w http.ResponseWriter, r *http.Request) {
// Проверяем, что запрос DELETE
if r.Method != http.MethodDelete {
http.Error(w, "Только DELETE", http.StatusMethodNotAllowed)
return
}
// Удаляем
w.WriteHeader(http.StatusNoContent) // 204
}
// Или с Go 1.22+
mux := http.NewServeMux()
mux.HandleFunc("DELETE /users", deleteUserHandler)
Другие методы (коротко)
- HEAD — как GET, но без тела ответа (только заголовки). Используют для проверки существования.
- OPTIONS — спрашивает, какие методы разрешены для пути. Браузеры используют для CORS.
- TRACE — диагностика (редко).
- CONNECT — для туннелей (proxies).
Сравнительная таблица
| Метод | Меняет сервер? | Идемпотентный? | Тело запроса | Типичный статус ответа | Пример использования |
|---|---|---|---|---|---|
| GET | Нет | Да | Нет | 200 OK | Получить список пользователей |
| POST | Да | Нет | Да | 201 Created | Создать нового пользователя |
| PUT | Да | Да | Да | 200 OK или 201 Created | Полная замена профиля |
| PATCH | Да | Иногда | Да | 200 OK | Изменить только email |
| DELETE | Да | Да | Обычно нет | 204 No Content | Удалить пользователя |
Лучшие практики в Go
с Go 1.22+ роутингом:
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("PATCH /users/{id}", patchUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)
Ранняя обработка методов запросов:
func userHandler(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
// получить пользователя
case http.MethodPut:
// полностью заменить
case http.MethodPatch:
// частично обновить
case http.MethodDelete:
// удалить
default:
http.Error(w, "Метод не разрешён", http.StatusMethodNotAllowed)
}
}
Динамические пути
В чистом net/http не было встроенного роутинга с переменными до Go 1.22. С 1.22 появился паттерн-роутинг и актуальный вариант выглядит так:
mux := http.NewServeMux()
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id") // "42"
fmt.Fprintf(w, "Пользователь с ID: %s", id)
})
mux.HandleFunc("GET /files/{path...}", func(w http.ResponseWriter, r *http.Request) {
path := r.PathValue("path") // остаток пути
fmt.Fprintf(w, "Файл: %s", path)
})
http.ListenAndServe(":8080", mux)
Для старых версий — ручная проверка
func userHandler(w http.ResponseWriter, r *http.Request) {
path := r.URL.Path
if len(path) <= 8 || path[:8] != "/users/" {
http.NotFound(w, r)
return
}
id := path[8:]
fmt.Fprintf(w, "Пользователь ID: %s", id)
}
http.HandleFunc("/users/", userHandler)
Читаем данные из запроса
Query-параметры (?name=Аня&age=25)
Query-параметры — это дополнительные данные, которые добавляются в конец URL после знака ?, чтобы передать серверу настройки или фильтры для запроса. Они выглядят так: https://example.com/search?q=golang&page=2&limit=20, где q=golang — поисковый запрос, page=2 — номер страницы, limit=20 — сколько результатов показать, а параметры разделяются символом &.
Они нужны в основном для GET-запросов, когда ты хочешь получить данные с уточнениями (поиск, сортировка, пагинация, фильтры по цене или категории) — это делает URL понятным человеку, позволяет копировать и делиться ссылками с готовыми настройками, а браузер и серверы могут кэшировать разные варианты.
Query-параметры — это удобный способ "договориться" с сервером о том, какие именно данные тебе нужны, без создания отдельного пути для каждого случая.
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
values := r.URL.Query() // map[string][]string
name := values.Get("name") // первое значение или ""
age := values.Get("age")
category := values["category"] // []string — все значения
fmt.Fprintf(w, "Имя: %s, Возраст: %s, Категории: %v", name, age, category)
})
Тестирование с помощью curl:
curl -X GET http://localhost:8080/users?name=Аня&age=25&category=1,2
# Имя: Аня, Возраст: 25, Категории: [1 2]
Заголовки (Headers)
HTTP-заголовки (Headers) — это дополнительные строки в запросе или ответе, которые передают важную информацию о самом запросе/ответе, но не являются основным содержимым (телом).
Представь письмо: тело письма — это текст сообщения, а заголовки — это конверт с данными: от кого, кому, дата, тип содержимого, язык и т.д. Например, заголовок Content-Type: application/json говорит «в теле JSON», Authorization: Bearer token передаёт токен авторизации, User-Agent рассказывает, какой браузер или программа сделала запрос, а Cache-Control управляет кэшированием.
Заголовки нужны, чтобы клиент и сервер могли договориться о формате данных, авторизации, кэшировании, языке, сжатии и многом другом — без них HTTP работал бы очень ограниченно, и многие современные возможности (авторизация, CORS, кэш) были бы невозможны.
Заголовки — это «метка» на посылке, которая помогает правильно её обработать.
mux.HandleFunc("GET /users/{id}", func(w http.ResponseWriter, r *http.Request) {
auth := r.Header.Get("Authorization")
userAgent := r.Header.Get("User-Agent")
contentType := r.Header.Get("Content-Type")
fmt.Fprintf(w, "Auth: %s\nUser-Agent: %s\nContent-Type: %s", auth, userAgent, contentType)
})
Тестирование с помощью cURL:
curl -X GET http://localhost:8080/users/123 -H "Authorization: Bearer token123" -H "User-Agent: MyClient/1.0" -H "Content-Type: application/json"
# Auth: Bearer token123
# User-Agent: MyClient/1.0
# Content-Type: application/json%
Формы (application/x-www-form-urlencoded)
Формы — это способ отправить данные с веб-страницы на сервер, обычно через POST-запрос.
Представь обычную форму на сайте: поля для логина, пароля, комментария или поиска — ты заполняешь их и нажимаешь "Отправить". Браузер собирает все введённые данные и отправляет их серверу либо в теле запроса (как ключ=значение), либо иногда в URL. Они нужны, чтобы пользователь мог взаимодействовать с сайтом: регистрироваться, входить в аккаунт, отправлять сообщения, делать заказы, загружать файлы или искать информацию.
Без форм веб был бы только для просмотра статичных страниц — формы делают интернет интерактивным и позволяют серверу получать данные от пользователя.
func formHandler(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost {
r.ParseForm() // парсим форму
login := r.FormValue("login")
password := r.FormValue("password")
fmt.Fprintf(w, "Логин: %s, Пароль: %s", login, password)
} else {
// Показываем форму
fmt.Fprint(w, `
<form method="post">
Логин: <input name="username"><br>
Пароль: <input name="password" type="password"><br>
<button>Войти</button>
</form>
`)
}
}
mux.HandleFunc("GET /users", formHandler)
mux.HandleFunc("POST /users", formHandler)
Тестирование POST-метода с помощью cURL (Метод GET работает в браузере):
curl -X POST http://localhost:8080/users -d "login=anya&password=secret"
# Логин: anya, Пароль: secret
JSON в теле (POST/PUT)
JSON — это простой и лёгкий формат для хранения и передачи данных, который выглядит как текст и легко читается как человеком, так и компьютером. Он состоит из пар «ключ: значение» (как словарь), массивов (списков) и вложенных объектов — например: {"name": "Аня", "age": 25, "hobbies": ["книги", "спорт"]}.
JSON нужен, чтобы разные программы, сервисы и языки программирования могли обмениваться данными: браузер отправляет JSON на сервер, сервер отвечает JSON'ом, мобильное приложение получает данные в JSON, API (как погода или карты) возвращают JSON. Без него было бы сложно "договориться" между фронтендом, бэкендом и внешними сервисами — JSON стал универсальным "языком" интернета для передачи структурированной информации.
JSON — это удобный текстовый контейнер для данных, который все понимают.
type CreateUserRequest struct {
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
func main(){
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
err := json.NewDecoder(r.Body).Decode(&req)
if err != nil {
http.Error(w, "Неверный JSON: "+err.Error(), http.StatusBadRequest)
return
}
// Здесь сохраняем в БД...
fmt.Fprintf(w, "Создан пользователь: %s (%d лет)", req.Name, req.Age)
})
}
Тестирование POST с помощью cURL:
curl -X POST http://localhost:8080/users \
-H "Content-Type: application/json" \
-d '{"name": "anya", "email": "anya@example.com", "age": 25}'
# Создан пользователь: anya (25 лет)
Возаращаем разные ответы
Текст и HTML
Кроме простых строк и JSON формата, можно использовать HTML (Чаще используется пакет html/template для работы с html)
mux.HandleFunc("GET /users", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "<h1>Заголовок</h1><p>Текст</p>")
})
JSON
JSON является самым распространённым форматом ответов от сервера
type APIResponse struct {
Success bool `json:"success"`
Data any `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
// Удобный кастомный метод для отправки JSON ответа от сервера
func sendJSON(w http.ResponseWriter, data any, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
// Использование
sendJSON(w, APIResponse{Success: true, Data: user}, http.StatusOK)
sendJSON(w, APIResponse{Success: false, Error: "Не найдено"}, http.StatusNotFound)
Заголовки и статусы
w.Header().Set("Content-Type", "text/plain")
w.Header().Set("X-Custom-Header", "my-value")
w.WriteHeader(http.StatusCreated) // 201
fmt.Fprint(w, "Создано")
Редиректы
Редирект — это перенаправление пользователя на другой URL.
http.Redirect(w, r, "/login", http.StatusFound) // 302
http.Redirect(w, r, "/dashboard", http.StatusPermanentRedirect) // 308
Отправка файлов и статики
// Один файл
http.ServeFile(w, r, "static/resume.pdf")
// Папка со статичными файлами
fileServer := http.FileServer(http.Dir("static"))
http.Handle("/static/", http.StripPrefix("/static/", fileServer))
Статичные файлы — это файлы, которые не изменяются в процессе работы приложения. Они хранятся на диске и отправляются клиенту при запросе.
Middleware — сквозная логика для всех хендлеров
Middleware — это специальные функции-обёртки, которые "оборачивают" твой основной хендлер (обработчик запроса) и добавляют к нему полезную логику, которая должна выполняться для многих или всех запросов.
Представь, что у тебя есть дверь в комнату (хендлер), а middleware — это охранники или помощники перед дверью: один проверяет пропуск (авторизация), другой записывает, кто пришёл (логирование), третий надевает на гостя тапочки (добавляет заголовки CORS), четвёртый измеряет время посещения (метрики). Каждый middleware получает запрос, может что-то с ним сделать, передать дальше следующему middleware или хендлеру, а потом обработать ответ. Они нужны, чтобы не писать одну и ту же повторяющуюся логику (логи, авторизация, сжатие, восстановление после паники) в каждом хендлере отдельно — вместо этого пишешь её один раз в middleware и "надеваешь" на нужные маршруты или весь сервер.
Middleware — это удобный способ сделать код чище, повторно используемым и добавить сквозную функциональность ко всем запросам. 😊
// Логирование
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
duration := time.Since(start)
log.Printf("%s %s %v", r.Method, r.URL.Path, duration)
})
}
// Авторизация по токену
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret-token" {
http.Error(w, "Доступ запрещён", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
Цепочка middleware
mux := http.NewServeMux()
// ... регистрируем хендлеры
handler := loggingMiddleware(authMiddleware(mux))
http.ListenAndServe(":8080", handler)
CORS
CORS (Cross-Origin Resource Sharing) — это механизм безопасности в браузерах, который по умолчанию запрещает веб-странице (например, сайту на домене example.com) делать запросы к другому домену (api.other.com), чтобы защитить пользователей от злонамеренных сайтов, которые могли бы красть данные.
Без CORS браузер просто блокирует такой запрос и выдаёт ошибку. Сервер может разрешить доступ, отправляя специальные заголовки в ответе (например, Access-Control-Allow-Origin: https://example.com или * для всех), указывая, какие домены, методы (GET, POST) и заголовки разрешены. CORS нужен, чтобы современный веб работал: фронтенд на одном домене мог безопасно общаться с API на другом, но только если сервер явно это разрешит.
CORS — это "пропуск" от сервера браузеру: "Да, этому сайту можно ко мне обращаться".
// 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")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Использование CORS в middleware
mux := http.NewServeMux()
// ... регистрируем хендлеры
handler := corsMiddleware(mux)
// Либо же цепочка middleware
handler := loggingMiddleware(authMiddleware(handler))
http.ListenAndServe(":8080", handler)
Полноценный REST API — собираем всё вместе
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
"time"
)
// Структура пользователя
type User struct {
ID int `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
Age int `json:"age"`
}
// Объявляем глобальные переменные
var (
users = make(map[int]User)
nextID = 1
mu sync.RWMutex
)
func main() {
mux := http.NewServeMux()
// API маршруты
mux.HandleFunc("GET /api/users", listUsers)
mux.HandleFunc("POST /api/users", createUser)
mux.HandleFunc("GET /api/users/{id}", getUser)
mux.HandleFunc("PUT /api/users/{id}", updateUser)
mux.HandleFunc("DELETE /api/users/{id}", deleteUser)
// Статическая папка
mux.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.Dir("static"))))
// Middleware цепочка
handler := corsMiddleware(loggingMiddleware(mux))
log.Println("API сервер запущен на :8080")
log.Fatal(http.ListenAndServe(":8080", handler))
}
// loggingMiddleware логирует запросы
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf("→ %s %s", r.Method, r.URL.Path)
next.ServeHTTP(w, r)
log.Printf("← %s %s %v", r.Method, r.URL.Path, time.Since(start))
})
}
// corsMiddleware обеспечивает 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 == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
// === Хендлеры ===
// listUsers получает список пользователей
func listUsers(w http.ResponseWriter, r *http.Request) {
// Используем мьютекс для безопасного доступа к данным и чтобы избежать гонок данных
mu.RLock()
defer mu.RUnlock()
list := make([]User, 0, len(users))
for _, u := range users {
list = append(list, u)
}
sendJSON(w, map[string]any{"users": list}, http.StatusOK)
}
// createUser создает нового пользователя
func createUser(w http.ResponseWriter, r *http.Request) {
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
sendJSON(w, map[string]string{"error": "Неверный JSON"}, http.StatusBadRequest)
return
}
mu.Lock()
u.ID = nextID
nextID++
users[u.ID] = u
mu.Unlock()
sendJSON(w, u, http.StatusCreated)
}
// getUser получает пользователя по ID
func getUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
sendJSON(w, map[string]string{"error": "Неверный ID"}, http.StatusBadRequest)
return
}
mu.RLock()
user, exists := users[id]
mu.RUnlock()
if !exists {
sendJSON(w, map[string]string{"error": "Не найдено"}, http.StatusNotFound)
return
}
sendJSON(w, user, http.StatusOK)
}
// updateUser обновляет пользователя по ID
func updateUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
sendJSON(w, map[string]string{"error": "Неверный ID"}, http.StatusBadRequest)
return
}
var u User
if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
sendJSON(w, map[string]string{"error": "Неверный JSON"}, http.StatusBadRequest)
return
}
mu.Lock()
if _, exists := users[id]; !exists {
// Явно указываем на Unlock мьютекса, т.к. при defer Unlock в данном может не быть вызван [Пояснение ниже]
mu.Unlock()
sendJSON(w, map[string]string{"error": "Не найдено"}, http.StatusNotFound)
return
}
u.ID = id
users[id] = u
mu.Unlock()
sendJSON(w, u, http.StatusOK)
}
// deleteUser удаляет пользователя по ID
func deleteUser(w http.ResponseWriter, r *http.Request) {
idStr := r.PathValue("id")
id, err := strconv.Atoi(idStr)
if err != nil {
sendJSON(w, map[string]string{"error": "Неверный ID"}, http.StatusBadRequest)
return
}
mu.Lock()
if _, exists := users[id]; !exists {
mu.Unlock()
sendJSON(w, map[string]string{"error": "Не найдено"}, http.StatusNotFound)
return
}
delete(users, id)
mu.Unlock()
sendJSON(w, map[string]string{"message": "Удалено"}, http.StatusOK)
}
// sendJSON утилита для отправки JSON ответа
func sendJSON(w http.ResponseWriter, data any, status int) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
Почему мы явно вызываем mu.Unlock(), а не defer?
При return в ветке if функция завершается сразу, и управление выходит из неё. Но вот в чём нюанс с defer:
defer не выполняется сразу при return — он выполняется только в момент реального выхода из функции, то есть после того, как все инструкции до return выполнятся, но перед тем, как стек фрейм функции окончательно очистится.
В этом случае:
mu.Lock()
defer mu.Unlock() // ← Это поставит Unlock в очередь на выполнение при выходе
if _, exists := users[id]; !exists {
sendJSON(...) // ← Выполнится
return // ← Функция "хочет" завершиться
}
// ← Но defer mu.Unlock() сработает ТОЛЬКО ЗДЕСЬ, после return!
То есть:
- Выполняется
sendJSON(...) - Выполняется
return— функция начинает завершаться - Только потом срабатывает
defer mu.Unlock() - Мьютекс разблокируется
В итоге мьютекс остаётся заблокированным на время выполнения sendJSON и всего, что связано с return. Это плохо: другие горутины ждут разблокировки, а ты держишь мьютекс дольше необходимого.
Вариант с явным mu.Unlock() перед return — правильный, потому что:
- Ты разблокировал мьютекс сразу, как только он больше не нужен.
- Мьютекс держится ровно столько, сколько требуется для проверки и записи.
- Нет лишней задержки.
returnзавершает функцию, ноdeferсрабатывает в самую последнюю очередь. Поэтому при раннихreturnвнутри критической секции (где естьLock) — нельзя полагаться наdefer Unlock(). Нужно разблокировать вручную передreturn.
Graceful shutdown — красивое завершение сервера
Graceful shutdown — это красивое завершение сервера, которое позволяет серверу завершить все текущие запросы и отключиться без потери данных.
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func runServer() {
srv := &http.Server{
Addr: ":8080",
Handler: handler, // твой mux с middleware
}
// Запуск в горутине
go func() {
log.Println("Сервер запущен на :8080")
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Ошибка сервера: %v", err)
}
}()
// Ловим сигналы завершения
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
log.Println("Получен сигнал завершения...")
// Таймаут на завершение
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Ошибка при shutdown: %v", err)
} else {
log.Println("Сервер gracefully остановлен")
}
}
Лучшие практики 2026 года
- Используй
http.NewServeMuxс паттернами (Go 1.22+). - Всегда проверяй метод и возвращай правильные статусы.
- Обрабатывай ошибки парсинга (JSON, формы).
- Устанавливай Content-Type.
- Middleware — для логов, CORS, авторизации, паник-рекавери.
- Graceful shutdown — обязательно в продакшене.
- Context — для отмены долгих операций (БД, внешние API).
- Лимиты на тело запроса —
r.Body = http.MaxBytesReader(w, r.Body, 10<<20)(10 MB). - HTTPS — в проде используй TLS:
srv.ListenAndServeTLS(cert, key).