Опирается на правила:
R-OAS-1..4,R-URL-X1..X4,R-MTH-X1,R-NEST-X1..X3,R-VER-X1..X4,R-QRY-X1..X5,R-RSP-X1..X4,R-HDR-X1,R-ERR-X1..X4,R-RATE-X1,R-DEP-X1,R-ALIAS-X1..X2,R-ACT-X1..X2,R-PRIN-X1,R-LOC-X1→ раздел OpenAPI-метаданные и антипаттерны.
Важно знать
- В Go нет встроенной генерации OpenAPI — спека ведётся через
swaggo/swag-аннотации или вручную рядом с кодом; структуры Go — источник контракта.operationId— camelCase, форматдействие + ресурс(createOrder,confirmOrder); без него SDK-генератор создаёт имена видаpostOrdersIdConfirm.tags— один тег на ресурс, множественное число с заглавной (Orders,Products); action-эндпоинты получают тег родительского ресурса.- Параметры пути в chi используют
{id}(контекст — из имени сегмента); в OpenAPI-спеке параметры именуются уникально (orderId,itemId) — Swagger UI/Redoc не работают с одинаковыми именами.omitemptyна всех необязательных полях response-структур закрываетR-RSP-X1(нетnullв 2xx);*T-указатели в response тянутnull— только для PATCH-запросов.- Валидация →
400 Bad Requestчерезgo-playground/validator+violations;422— за пределами допустимого списка кодов (R-ERR-X3).- Единый
httperr.Writeчерез весь сервис закрываетR-PRIN-2(единообразие); разные структуры ошибок в разных хендлерах — антипаттерн.
operationId
R-OAS-1: каждая операция получает уникальный operationId в camelCase. Паттерн: действие + ресурс.
// createOrder godoc
// @Summary Создать заказ
// @Tags Orders
// @Accept json
// @Produce json
// @Param body body CreateOrderRequest true "Параметры заказа"
// @Success 201 {object} OrderResponse
// @Failure 400 {object} ValidationProblem
// @Failure 409 {object} ProblemDetails
// @Failure 500 {object} ProblemDetails
// @Router /api/v1/orders [post]
// @operationId createOrder
func createOrder(w http.ResponseWriter, r *http.Request) { ... }
// confirmOrder godoc
// @Summary Подтвердить заказ
// @Tags Orders
// @Param orderId path string true "ID заказа"
// @Success 200 {object} OrderResponse
// @Failure 404 {object} ProblemDetails
// @Failure 409 {object} ProblemDetails
// @Router /api/v1/orders/{orderId}/confirm [post]
// @operationId confirmOrder
func confirmOrder(w http.ResponseWriter, r *http.Request) { ... }
Конвенция именования:
| Операция | operationId |
|---|---|
GET /orders | getOrders |
GET /orders/{id} | getOrder |
POST /orders | createOrder |
PUT /orders/{id} | updateOrder |
PATCH /orders/{id} | patchOrder |
DELETE /orders/{id} | deleteOrder |
POST /orders/{id}/confirm | confirmOrder |
POST /orders/search | searchOrders |
GET /products/{id}/price-history | getProductPriceHistory |
POST /customers/{id}/verify | verifyCustomer |
SDK-генераторы (openapi-generator) используют operationId как имя метода. Без него генерируется что-то вроде postApiV1OrdersOrderIdConfirm — нечитаемо и нестабильно при изменении маршрута.
tags
R-OAS-2: один тег на ресурс. Action-эндпоинты — тег родительского ресурса.
// @Tags Orders ← createOrder, getOrder, confirmOrder, cancelOrder
// @Tags Products ← getProduct, createProduct, listProductReviews
// @Tags Customers ← getCustomer, verifyCustomer
В chi-роутере тег соответствует блоку r.Route:
r.Route("/api/v1", func(r chi.Router) {
r.Route("/orders", func(r chi.Router) { // тег Orders
r.Get("/", listOrders) // getOrders
r.Post("/", createOrder) // createOrder
r.Route("/{id}", func(r chi.Router) {
r.Get("/", getOrder) // getOrder
r.Post("/confirm", confirmOrder) // confirmOrder → тег Orders
r.Post("/cancel", cancelOrder) // cancelOrder → тег Orders
})
})
r.Route("/products", func(r chi.Router) { // тег Products
r.Get("/", listProducts)
r.Get("/{id}", getProduct)
})
})
Swagger UI группирует эндпоинты по тегам. Без тегов или с плоским списком — 50+ операций без навигации.
Параметры пути: {id} в chi vs уникальные имена в OpenAPI
R-OAS-3 и R-NEST-4 задают двойной стандарт:
// chi-маршрут — {id} в дизайне URL, контекст даёт сегмент ресурса
r.Get("/orders/{id}/items/{id}", getOrderItem) // ← так в дизайне
// chi-обработчик — читаем chi.URLParam по имени
orderID := chi.URLParam(r, "id") // первый {id} — orderId по контексту
itemID := chi.URLParam(r, "id") // второй {id} — itemId по контексту
В chi параметры с одинаковым именем в одном пути — ошибка маршрутизации. Реальный роутер:
r.Get("/orders/{orderId}/items/{itemId}", getOrderItem)
В OpenAPI-спеке — уникально всегда:
/api/v1/orders/{orderId}/items/{itemId}:
get:
operationId: getOrderItem
parameters:
- name: orderId
in: path
required: true
schema:
type: string
format: uuid
- name: itemId
in: path
required: true
schema:
type: string
format: uuid
Swagger UI/Redoc не рендерят операцию корректно, если два параметра в одном пути называются одинаково — требование инструмента, не семантическое.
summary и description
R-OAS-4: summary — обязателен, до 80 символов; description — только если логика неочевидна.
// getSberCardLimit godoc
// @Summary Получить лимит карты Сбера
// @Description Возвращает текущий расходный лимит по карте клиента.
// Лимит пересчитывается ночью; в течение дня — кешированное значение.
// @Tags Customers
// @Param customerId path string true "ID клиента"
// @Success 200 {object} CardLimitResponse
// @Router /api/v1/customers/{customerId}/card-limit [get]
// @operationId getCustomerCardLimit
func getCustomerCardLimit(w http.ResponseWriter, r *http.Request) { ... }
Пустой description лучше, чем дублирование summary. Description нужен когда: есть побочные эффекты, поведение зависит от роли, или семантика неочевидна из названия.
Структуры Go как источник контракта
В Go нет reflection-based OpenAPI-генерации уровня Spring или FastAPI. Два варианта синхронизации:
Вариант А — swaggo/swag: аннотации в комментариях, swag init генерирует docs/swagger.json. Плюс: спека всегда рядом с кодом. Минус: аннотации дублируют структуры.
Вариант Б — ручная спека: openapi.yaml рядом с main.go. Структуры Go — источник правды; ревьюер проверяет соответствие до мержа.
В обоих вариантах структуры response-типов определяют контракт:
type OrderResponse struct {
OrderID string `json:"orderId"`
Status string `json:"status"` // NEW | CONFIRMED | SHIPPED
CreatedAt time.Time `json:"createdAt"`
TotalRub int64 `json:"totalRub"` // копейки
Items []ItemResponse `json:"items"`
Note string `json:"note,omitempty"` // omitempty — поле отсутствует если пусто
}
type PageResponse[T any] struct {
Content []T `json:"content"`
Page int `json:"page"`
Size int `json:"size"`
Total int `json:"total"`
}
omitempty на Note означает: поле отсутствует в JSON, когда пусто — нет "note": null, нет "note": "". Это одновременно закрывает R-RSP-X1 и R-RSP-X2.
Антипаттерны: сводная таблица
Полный чеклист для ревью — все X-коды контракта в идиомах Go.
URL и маршрутизация
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Глагол в URL для CRUD (/getOrders, /createProduct) | R-URL-X4 | GET /orders, POST /products |
CamelCase или snake_case в пути (/orderItems, /order_items) | R-URL-X1 | /order-items |
Trailing slash в chi (r.Get("/orders/", ...)) | R-URL-X2 | r.Get("/orders", ...) |
Расширение в пути (/orders.json) | R-URL-X3 | /orders + Accept: application/json |
ID в теле запроса вместо chi.URLParam | R-NEST-X2 | orderId := chi.URLParam(r, "orderId") |
Три уровня вложенности (/orders/{id}/items/{id}/shipments) | R-NEST-X1 | GET /shipments?itemId={id} |
Единственное число коллекции (/order, /product) | R-RES-X1 | /orders, /products |
GET с побочным эффектом (GET /orders/{id}/confirm) | R-MTH-X1 | POST /orders/{id}/confirm |
Версионирование
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Версия в query (?version=2) | R-VER-X2 | r.Route("/api/v2", ...) |
Минорная версия (/api/v1.2/) | R-VER-X1 | /api/v2/ только при breaking change |
Эндпоинт без /api или без версии | R-VER-X3 | r.Route("/api/v1", ...) обязательно |
| Новая версия ради необязательного поля | R-VER-X4 | добавить omitempty-поле в текущую |
Query-параметры
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Comma-separated массивы (?status=NEW,PAID) | R-QRY-X3 | r.Form["status"] — повтор параметра |
page=0 или 0-based нумерация | R-QRY-X2 | page >= 1; дефолт — Page: 1 |
| snake_case или PascalCase в параметрах | R-QRY-X1 | camelCase: createdFrom, pageSize |
Бизнес-логика в query (?action=cancel) | R-QRY-X4 | POST /orders/{id}/cancel |
| Конструирование cursor на клиенте | R-QRY-X5 | cursor — непрозрачный токен, base64 или UUID |
JSON и ответы
| Антипаттерн | Правило | Что взамен |
|---|---|---|
*string-указатель в response-структуре → null в 2xx | R-RSP-X1 | string + omitempty; *string только в PATCH-запросе |
json.Marshal без omitempty на необязательных полях | R-RSP-X1 | добавить omitempty или не включать поле |
Пустая строка вместо отсутствия поля ("note": "") | R-RSP-X2 | string с omitempty |
Envelope {"data": {...}, "success": true} | R-RSP-X4 | плоский объект без обёртки |
"content": null для пустой коллекции | R-RSP-7 | "content": [] — инициализировать слайс |
float64 для денежных сумм | — | int64 (минорные единицы — копейки, центы) |
Заголовки
| Антипаттерн | Правило | Что взамен |
|---|---|---|
X--prefix в кастомных заголовках (X-Request-Id) | R-HDR-X1 | доменный префикс (Shop-Request-Id) |
Ошибки
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Content-Type: application/json для ошибок | R-ERR-X1 | application/problem+json |
type: "about:blank" в ProblemDetails | R-ERR-X2 | urn:problem:order-service:not-found |
| HTTP 422 для ошибок валидации | R-ERR-X3 | 400 Bad Request + code: VALIDATION_ERROR |
err.Error() из pgx/sqlc в поле detail | R-ERR-X4 | санитизировать через apperr.KindOf; в detail — общая фраза |
| Разные структуры ошибок в разных хендлерах | R-PRIN-2 | единый httperr.Write(w, r, err) через весь сервис |
Rate limiting и deprecation
| Антипаттерн | Правило | Что взамен |
|---|---|---|
429 без Retry-After и RateLimit-* | R-RATE-X1 | middleware выставляет все три заголовка |
deprecated: true в OpenAPI без Sunset | R-DEP-X1 | middleware с Sunset + Deprecation + Link |
Alias и actions
| Антипаттерн | Правило | Что взамен |
|---|---|---|
me для собственных ресурсов singleton (/users/me/settings) | R-ALIAS-X1 | /settings — контекст из токена |
| HATEOAS-ссылки в теле ответа | R-PRIN-X1 | OpenAPI описывает навигацию; только Location при создании |
Существительное в action (/cancellation, /confirmation) | R-ACT-X1 | /cancel, /confirm — глагол-инфинитив |
PUT или PATCH для action-эндпоинта | R-ACT-X2 | POST /orders/{id}/confirm |
OpenAPI-метаданные
| Антипаттерн | Правило | Что взамен |
|---|---|---|
Нет operationId или дублирование | R-OAS-1 | уникальный camelCase на каждой операции |
Action-эндпоинт в отдельном теге (OrderActions) | R-OAS-2 | тег родительского ресурса (Orders) |
| Одинаковые имена параметров в одном пути в OpenAPI | R-OAS-3 | {orderId}, {itemId} — уникально |
Нет summary на операции | R-OAS-4 | короткая фраза до 80 символов — обязательно |
Куда дальше
- go/url-and-resources.md — kebab-case, вложенность,
chi.URLParam - go/alias-and-actions.md —
me, action-эндпоинты,POST /confirm - go/versioning.md — breaking change, chi
r.Route("/api/v2", ...) - go/query-params.md — camelCase,
r.Form["status"], cursor - go/json-and-responses.md —
omitempty,PageResponse,int64 - go/errors.md —
ProblemDetails,httperr.Write,violations - go/headers.md —
Idempotency-Key,traceparent, доменный префикс - go/rate-limiting-files-deprecation.md —
429,Sunset,multipart - go/batch-async-localization.md — batch,
202 Accepted,Accept-Language - Ошибки — error-handling Go —
apperr.Kind,errors.As