Опирается на правила:
R-VER-1..6иR-VER-X1..X4→ раздел Версионирование.
Важно знать
- Версия в URL-пути:
/api/v1/orders. Формат —v+ целое число.- Префикс
/apiобязателен для всех бизнес-эндпоинтов.- Новая версия только при breaking change. Non-breaking — в текущей версии.
- Клиент обязан игнорировать неизвестные поля и enum-значения в ответе.
- chi монтирует версии через
r.Route("/api/v1", ...)иr.Route("/api/v2", ...).- Служебные (
/health,/metrics) — вне/api, без версии.- Минорная версия (
v1.2) или дата (/api/2024) — запрещены.- Версия в query (
?version=1) — запрещена.
Базовая структура роутера
R-VER-1..3:
r := chi.NewRouter()
r.Get("/health", healthHandler)
r.Get("/metrics", metricsHandler)
r.Route("/api/v1", func(r chi.Router) {
r.Use(authMiddleware, tracingMiddleware)
r.Route("/orders", ordersRouterV1)
r.Route("/products", productsRouterV1)
r.Route("/customers", customersRouterV1)
})
Параллельная поддержка v1 и v2
R-VER-4: breaking change → новая версия; v1 продолжает работать.
r.Route("/api/v1", func(r chi.Router) {
r.Route("/orders", ordersRouterV1)
})
r.Route("/api/v2", func(r chi.Router) {
r.Route("/orders", ordersRouterV2)
})
Обе версии могут разделять один слой use-cases; отличаются только DTO и маппинг:
func ordersRouterV1(r chi.Router) {
r.Get("/", listOrdersV1)
r.Post("/", createOrderV1)
// ...
}
func ordersRouterV2(r chi.Router) {
r.Get("/", listOrdersV2)
r.Post("/", createOrderV2)
// ...
}
func listOrdersV1(w http.ResponseWriter, r *http.Request) {
orders := svc.ListOrders(r.Context(), /* params */)
writeJSON(w, http.StatusOK, toOrderListResponseV1(orders))
}
func listOrdersV2(w http.ResponseWriter, r *http.Request) {
orders := svc.ListOrders(r.Context(), /* params */)
writeJSON(w, http.StatusOK, toOrderListResponseV2(orders))
}
Таблица breaking vs non-breaking
Breaking — требует v2
| Изменение | Пример |
|---|---|
| Удаление endpoint | убрали DELETE /orders/{id} |
| Удаление/переименование поля | customerId → clientId |
| Удаление/переименование query-параметра | ?status= → ?orderStatus= |
| Изменение типа поля | total: string → total: int64 |
| Удаление значения из enum | убрали OrderStatus.DRAFT |
| Изменение HTTP-метода | POST /orders → PUT /orders |
| Добавление обязательного поля в запросе | required deliverySlot |
| Ужесточение валидации | maxLength: 200 → maxLength: 50 |
Non-breaking — в текущей версии
| Изменение | Пример |
|---|---|
| Необязательное новое поле в ответе | добавили metadata в OrderResponse |
| Новое значение enum | OrderStatus.RESERVED |
| Новый endpoint | POST /orders/{id}/duplicate |
| Необязательный новый query-параметр | ?channel=web |
| Ослабление валидации | maxLength: 50 → maxLength: 200 |
| Новый error code | ORDER_LIMIT_EXCEEDED |
R-VER-X4: новую версию не создаём для добавления optional поля в ответ.
Клиент и forward compatibility
R-VER-5: клиент обязан игнорировать неизвестные поля.
В Go стандартный encoding/json игнорирует неизвестные поля по умолчанию — это правильный дефолт:
type OrderResponse struct {
OrderID string `json:"orderId"`
Status string `json:"status"`
Total int64 `json:"total"`
// при добавлении нового поля на сервере клиентский код не сломается
}
var resp OrderResponse
json.Unmarshal(body, &resp) // неизвестные поля игнорируются
Не используй DisallowUnknownFields() в клиентских десериализаторах — это нарушает forward compatibility.
Для enum — обрабатывай неизвестные значения как unknown:
type OrderStatus string
const (
StatusNew OrderStatus = "NEW"
StatusConfirmed OrderStatus = "CONFIRMED"
StatusUnknown OrderStatus = ""
)
func parseStatus(s string) OrderStatus {
switch OrderStatus(s) {
case StatusNew, StatusConfirmed:
return OrderStatus(s)
default:
return StatusUnknown // игнорируем неизвестное
}
}
Deprecation v1 после выхода v2
Когда v2 вышел в production, v1 помечается устаревшим через middleware (R-DEP-*):
func deprecatedMiddleware(sunset, successorURL string) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Sunset", sunset)
w.Header().Set("Deprecation", "true")
w.Header().Set("Link", `<`+successorURL+`>; rel="successor-version"`)
next.ServeHTTP(w, r)
})
}
}
r.Route("/api/v1", func(r chi.Router) {
r.Use(deprecatedMiddleware(
"Sat, 01 Jan 2027 00:00:00 GMT",
"https://api.example.com/api/v2",
))
r.Route("/orders", ordersRouterV1)
})
Подробнее о deprecation — go/rate-limiting-files-deprecation.md.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
/api/v1.2/orders | R-VER-X1 | /api/v2/orders |
/api/2024/orders | R-VER-X1 | /api/v1/orders |
/orders?version=2 | R-VER-X2 | /api/v2/orders |
/orders без /api и версии | R-VER-X3 | /api/v1/orders |
/v1/orders без /api | R-VER-X3 | /api/v1/orders |
| Новая версия для optional поля | R-VER-X4 | добавить в текущую |
DisallowUnknownFields() в клиенте | R-VER-5 | игнорировать unknown |
Куда дальше
- go/url-and-resources.md — структура маршрутов chi.
- go/rate-limiting-files-deprecation.md —
Sunset+ deprecation headers. - go/openapi-and-antipatterns.md —
operationId,tagsв спеке. - go/errors.md —
ErrorCodeenum расширение как non-breaking.