1. HTTP как контракт: запрос → ответ
Когда люди впервые слышат «HTTP», они часто думают: «ну это же про сайты». И да, сайты тоже. Но для разработчика HTTP — это прежде всего очень строгий способ договориться между двумя программами: одна отправляет запрос (client), другая отвечает (server). Этот договор называется контрактом, и он включает не только «какой URL дернуть», но и «какой метод», «какие коды успеха/ошибок», «какой формат тела», «что считается корректным вводом».
В Go это особенно приятно: Go исторически очень сильный язык для серверов и сетевого кода. Даже в материалах про эволюцию Go подчёркивается, что Go проектировали для написания серверов.
Ментальная модель «конверта»
Представьте, что HTTP — это почта.
- Письмо = запрос (request).
- Конверт = метаданные (метод, путь, заголовки).
- Содержимое письма = тело запроса (например JSON).
- Ответное письмо = ответ (response).
- Марка на ответе = статус‑код (200, 404, 500…), который говорит «что произошло» коротко и однозначно.
Звучит слегка олдскульно, но как ментальная модель работает идеально: сервер не «угадывает по настроению», а отвечает формально.
Схема запроса и ответа
sequenceDiagram
participant C as Client
participant S as Server
C->>S: HTTP Request (method + path + headers + body)
S->>S: validate + compute
S-->>C: HTTP Response (status + headers + body)
Самая важная мысль из этой схемы: на выходе всегда есть статус‑код, а тело ответа может быть, а может не быть. И клиент должен уметь жить по статус‑коду, а не пытаться «парсить текст ошибки глазами».
Endpoint: метод + путь
Чтобы перестать говорить про HTTP как про магию, удобно ввести маленький термин: endpoint — это конкретная «точка входа» в API. И состоит она из двух частей: HTTP‑метод (что делаем) и путь (с чем работаем).
Например, «получить список задач» часто выглядит как GET /api/v1/tasks.
Сейчас мы не запускаем сервер и не пишем обработчики. Мы делаем то, что делают взрослые инженеры в начале проекта: фиксируем контракт. И для этого можно даже написать пару структур в Go — не потому что нам «надо код», а чтобы мысли стали точнее.
Мини‑тип для описания endpoint
package main
type Endpoint struct {
Method string
Path string
}
var listTasks = Endpoint{Method: "GET", Path: "/api/v1/tasks"}
var createTask = Endpoint{Method: "POST", Path: "/api/v1/tasks"}
Здесь важна не «красота кода», а дисциплина мышления: endpoint — это именно пара (method, path). Если вы меняете метод, вы меняете смысл. Если вы меняете путь, вы меняете ресурс.
2. HTTP‑методы: смысл действия и обещания клиенту
Про методы удобно думать как про глаголы с очень строгими правилами. Если вы используете методы «как попало», то клиенту приходится угадывать поведение, а API становится похож на квест «угадай кнопку». Если же вы придерживаетесь стандартной семантики, клиент (и другие программисты) понимают поведение без чтения исходников сервера. Это экономит часы, нервы и количество сообщений «а почему тут 200, когда ошибка?».
Ниже — практичная таблица, которую реально держат в голове, когда проектируют API.
Таблица базовой семантики методов
| Метод | Идея (по‑человечески) | Обычно про что | Идемпотентность (идея) |
|---|---|---|---|
|
«Дай данные» | чтение | да |
|
«Создай / выполни действие» | создание/команда | обычно нет |
|
«Замени целиком» | полная замена ресурса | да |
|
«Измени частично» | частичное изменение | обычно да (если спроектировано аккуратно) |
|
«Удали» | удаление | да |
Пара комментариев, чтобы это не выглядело как сухая теория.
GET почти всегда безопасен в том смысле, что не должен менять состояние на сервере. Если GET вдруг создаёт задачу, списывает деньги и отправляет письмо маме — это не GET, это сюжет для фильма ужасов.
POST часто используют для создания: «создать задачу», «создать пользователя». Но иногда POST — это «команда», которая не укладывается в «заменить состояние». Например: «запустить пересчёт отчёта». В нашем учебном домене задач мы постараемся держать POST как «создать задачу», потому что так проще.
PUT и PATCH: если вы не уверены, не торопитесь их применять. Но понимать смысл важно: PUT — это «вот новая версия ресурса целиком», PATCH — «вот маленькое изменение».
DELETE — удалить. И тут начинается философия: что значит «удалить», если ресурс уже удалён? Обычно договор делают так, чтобы повтор DELETE не ломал систему.
3. Статус‑коды: язык ответа сервера без гадания по тексту
Если метод — это «что мы хотим», то статус‑код — это «что получилось». И это не второстепенная деталь, а часть контракта: клиент пишет свою логику, ориентируясь на коды. В хорошем API клиенту не нужно «прочитать строку ошибки» и понять, что вы имели в виду. Он видит 404 и понимает «не найдено». Он видит 400 и понимает «я что‑то отправил не так».
Здесь есть очень удобная ментальная модель: классы кодов.
- 2xx — успех,
- 4xx — проблема в запросе (клиент виноват или хотя бы клиент может исправить),
- 5xx — проблема на сервере (клиент не виноват).
Маленькая функция «класс кода»
package main
func statusClass(code int) int {
return code / 100
}
// 200 -> 2
// 404 -> 4
// 500 -> 5
Да, это школьная математика. Но она помогает: увидели statusClass == 4 — значит, это «ошибка запроса». Увидели statusClass == 5 — «что-то сломалось у нас».
Минимальный набор кодов, который нужно уверенно понимать
В рамках нашего будущего API задач нам хватит небольшой «базовой пятёрки» (плюс один):
| Код | Имя | Когда обычно используется | Есть ли тело ответа |
|---|---|---|---|
|
OK | успешное чтение/изменение | обычно да |
|
Created | ресурс создан | обычно да (часто возвращают созданный объект) |
|
No Content | успех без тела | нет (и это важно!) |
|
Bad Request | неверный ввод/формат/валидация | обычно да (ошибка в JSON) |
|
Not Found | ресурс не найден | обычно да |
|
Internal Server Error | внутренняя ошибка сервера | обычно да (но сообщение стабильное и безопасное) |
Критичный нюанс про 204: это не «200, но без настроения». Это явный сигнал: «успех, тела нет». Если вы вернёте 204 и при этом отправите JSON — некоторые клиенты его просто проигнорируют, а некоторые будут вести себя непредсказуемо. То есть вы сами себе устроите баг, а потом будете героически его чинить.
Быстрый хелпер «это успех?»
package main
func is2xx(code int) bool {
return code >= 200 && code <= 299
}
4. Идемпотентность: повторили запрос — мир не стал хуже
Слово «идемпотентность» звучит так, будто его придумали, чтобы пугать студентов на контрольных. На практике идея очень простая: если вы повторите запрос, итоговый эффект не должен измениться. Это не значит, что «ничего не меняется». Это значит: «повтор не добавит лишнего эффекта».
Пример из жизни: кнопка «удалить задачу». Если пользователь нажал её дважды (или сеть дёрнулась, и клиент повторил запрос), задача должна остаться удалённой, а не «удалиться два раза» (что бы это ни значило).
Идемпотентность важна из‑за реального мира: сеть нестабильна, соединения рвутся, клиент может не получить ответ и решить повторить запрос. Если вы проектируете API так, чтобы повтор был безопасен, вы уменьшаете количество странных дублей и «призрачных» состояний.
Грубое правило как первое приближение
Часто делают так: считают, что GET/PUT/PATCH/DELETE должны быть идемпотентными, а POST — нет (потому что «создать» дважды = два ресурса). Это не абсолютная истина, но хорошая стартовая дисциплина.
package main
func isIdempotent(method string) bool {
switch method {
case "GET", "PUT", "PATCH", "DELETE":
return true
default:
return false
}
}
Маленькая демонстрация, чтобы «пощупать»
package main
import "fmt"
func main() {
fmt.Println(isIdempotent("GET")) // true
fmt.Println(isIdempotent("POST")) // false
fmt.Println(isIdempotent("DELETE")) // true
}
Это, конечно, не «реальная безопасность API». Но это хороший способ перестать путать идемпотентность с «ничего не меняет».
5. Привязываем к домену: черновик HTTP‑контракта для задач
Сейчас мы сделаем важный шаг: переведём абстрактные HTTP‑слова в конкретный словарь нашего приложения. Мы уже давно живём в домене задач (tasks): добавляем задачу, смотрим список, отмечаем выполненной. В HTTP‑стиле мы хотим, чтобы путь описывал сущность, а метод описывал действие.
Договоримся, что API версионируем через путь. Это выглядит так: /api/v1/.... Версия в пути — это способ сказать: «контракт зафиксирован, и если мы его сломаем, то сделаем это в другой версии, а не внезапно в пятницу вечером».
Таблица endpoint’ов как черновик контракта
| Смысл операции | Метод | Путь | Успех |
|---|---|---|---|
| Список задач | |
|
|
| Создать задачу | |
|
|
| Получить задачу | |
|
|
| Отметить «done» | |
|
200 (или 204, если решим без тела) |
| Удалить задачу | |
|
|
Обратите внимание: мы пока не обсуждаем JSON‑форматы и единый формат ошибок (это тоже часть контракта, но это отдельная тема). Здесь мы тренируем «скелет»: метод, путь, статус успеха — чтобы у нас не было POST /getTask и GET /deleteTask. Мы пишем API так, чтобы его можно было читать как английский (ну или как очень странный английский).
Чуть кода, чтобы зафиксировать методы и не писать строки руками
Ручные строки "GET" и "POST" работают, но легко ошибиться. Поэтому даже на раннем этапе полезно завести константы.
package main
type Method string
const (
MethodGet Method = "GET"
MethodPost Method = "POST"
MethodPut Method = "PUT"
MethodPatch Method = "PATCH"
MethodDelete Method = "DELETE"
)
А теперь endpoint’ы уже можно описывать «типизированно»:
package main
type Endpoint struct {
Method Method
Path string
}
var listTasks = Endpoint{Method: MethodGet, Path: "/api/v1/tasks"}
var deleteTask = Endpoint{Method: MethodDelete, Path: "/api/v1/tasks/{id}"}
6. Типичные ошибки при проектировании HTTP‑API
Ошибка №1: менять состояние через GET.
Обычно это происходит «из удобства»: «ну я же просто по ссылке открою…». Но GET — это чтение. Если GET меняет состояние, вы ломаете кэширование, ломаете повторные запросы и удивляете всех клиентов. В какой‑то момент вы поймаете баг «у нас задача создаётся сама» — и это будет не магия, а GET.
Ошибка №2: всегда отвечать 200 и прятать ошибки в тексте.
Иногда делают так: статус 200, а внутри JSON поле "error": "...". Для клиента это ужасно: ему нужно парсить тело, чтобы понять, успех это или нет. Правильнее, чтобы успех был 2xx, ошибка запроса была 4xx, а внутренняя ошибка сервера была 5xx. Тело ошибки может быть структурированным, но код — первичен.
Ошибка №3: путать 400 и 404.
Если клиент прислал id = "abc" и вы не можете разобрать его как число — это не «не найдено», это «неверный запрос», то есть 400. А 404 — это когда id корректный, но такого ресурса нет. Это различие сильно упрощает жизнь клиентам и тестам.
Ошибка №4: отдавать тело ответа при 204.
204 означает «успех, но тела нет». Если вы всё же отправите JSON, часть клиентов его проигнорирует. Получится API, которое «иногда работает». А «иногда работает» — это особый вид боли, потому что отлаживать нечего: оно же «иногда».
Ошибка №5: не думать про идемпотентность и повторы.
В реальном мире запросы повторяются. Если вы проектируете операции так, что повтор DELETE внезапно превращается в 500, или повтор PATCH создаёт дублирующие изменения, то клиенты будут бояться ретраев, а вы будете ловить «странные» состояния. Идемпотентность — это не украшение, а страховка от сетевой реальности.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ