33. Документация API

В классической традиции Bell Labs и Unix документация никогда не была оторванным от жизни канцелярским отчетом: справочные страницы man писались теми же авторами, что создавали системные вызовы ядра и стандартные утилиты C. Однако в современной распределенной микросервисной архитектуре документация приобрела совершенно новое качество — она перестала быть текстом для человеческого чтения и превратилась в машиночитаемый исполняемый контракт.

Спецификация OpenAPI (бывший Swagger) — это не просто красивая веб-страница со списком эндпоинтов. Это единый источник истины (Single Source of Truth), на основе которого автоматически генерируются строго типизированные клиентские SDK для мобильных приложений на Swift и Kotlin, настраиваются правила маршрутизации и валидации на пограничных API Gateway (Kong, Envoy) и прогоняются контрактные тесты.

В экосистеме Go подход к документированию разительно отличается от динамических платформ (Python/FastAPI или Java/Spring): здесь полностью отвергается медленная генерация через рефлексию в рантайме в пользу статической компиляционной генерации и нулевого влияния на скорость обработки запросов.

Философия документации API в Go

В распределенных системах документация — это не текст для чтения, а машиночитаемый контракт. В эпоху микросервисов, API-шлюзов и автогенерации клиентских SDK, OpenAPI/Swagger спецификация становится единым источником истины (Single Source of Truth). В Go-экосистеме подход к документации радикально отличается от PHP или Python: здесь предпочтение отдается статической генерации, компиляционной проверке и минимальному влиянию на рантайм-производительность.

1. Подходы: Code-First vs Spec-First

Выбор стратегии определяет архитектуру проекта, скорость разработки и надежность контрактов.

  • Code-First: Разработчик пишет код и аннотации в комментариях к хендлерам, а утилита генерирует OpenAPI-спецификацию. Прекрасный быстрый старт для молодых команд, но несет риск постепенного рассинхронизирования контракта (spec drift).
  • Spec-First: Спецификация изначально пишется вручную архитекторами в формате YAML/JSON, после чего кодогенератор строит серверные интерфейсы и клиентские DTO-типы. Гарантирует безупречное соответствие контракту, но требует более высоких инвестиций на начальном этапе.
graph TD
    A["Исходный код с комментариями"] --> B["Парсинг AST и извлечение тегов"]
    B --> C["Генерация swagger.json<br>и Go-структур"]
    C --> D["Встраивание через embed<br>в бинарный файл"]
    D --> E["HTTP сервер отдает статику"]
    E --> F["Swagger UI рендерит контракт"]

    classDef normal fill:#1e293b,stroke:#38bdf8,color:#f8fafc;
    classDef build fill:#0f172a,stroke:#10b981,color:#f8fafc;
    classDef ui fill:#334155,stroke:#f59e0b,color:#f8fafc;
    class A,D,E normal;
    class B,C build;
    class F ui;
  

2. Идиоматичная интеграция

В production-проектах спецификация встраивается напрямую в бинарник через директиву //go:embed, что исключает чтение файлов с диска и ускоряет деплой.

Go
// cmd/server/docs.go
package docs

import (
    "embed"
    "io/fs"
    "net/http"
)

//go:embed swagger.json docs/*
var docsFS embed.FS

// NewHandler возвращает HTTP-обработчик для Swagger UI
func NewHandler() http.Handler {
    // Отдаем корень как index.html (Swagger UI)
    staticFS, _ := fs.Sub(docsFS, "docs")
    return http.FileServer(http.FS(staticFS))
}

// Инициализация при старте (вызывается автоматически при импорте)
func init() {
    // swag сгенерировал init() с привязкой SwaggerInfo к скомпилированному JSON
    // docs.SwaggerInfo.ReadDoc() теперь работает без I/O
}

Интеграция с роутером:

Go
package main

import (
    "net/http"
    "myproject/internal/docs"
)

func setupRoutes() http.Handler {
    mux := http.NewServeMux()

    // Статические эндпоинты документации
    mux.Handle("/docs/", http.StripPrefix("/docs/", docs.NewHandler()))
    mux.HandleFunc("/docs/swagger.json", func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Content-Type", "application/json")
        w.Write(docs.SwaggerInfo.ReadDoc()) // Возвращает []byte из embed
    })

    // Бизнес-маршруты...
    return mux
}

3. Выравнивание документации и валидации

Самая частая проблема: документация обещает одно, а валидатор (github.com/go-playground/validator) проверяет другое. В Go это решается через единые структуры тегов.

Go
type CreateUserRequest struct {
    // swagger:required true
    // @Description Full name of the user
    Name string `json:"name" validate:"required,min=2,max=100" example:"John Doe"`

    // swagger:required false
    Email string `json:"email" validate:"required,email" example:"user@example.com"`
}

При генерации swag читает example, description и validate-теги, автоматически выставляя required в OpenAPI-схеме. Это гарантирует, что 400 Bad Request в документации совпадает с реальной логикой из статьи 7. Валидация входных данных.

4. Spec-First и oapi-codegen

Для enterprise-систем предпочтителен подход, где спецификация контролируется отдельно. Генератор github.com/oapi-codegen/oapi-codegen (ранее deepmap/oapi-codegen) генерирует:

  • Типизированные структуры запросов/ответов
  • Интерфейсы ServerInterface, которые должен реализовать сервис
  • Клиентский код для внешних вызовов
Go
// Генерируется автоматически из openapi.yaml
type ServerInterface interface {
    CreateUser(ctx echo.Context, request CreateUserJSONRequestBody) error
    GetUser(ctx echo.Context, userId int) error
}

// Реализация в сервисе
type APIServer struct{}

func (s *APIServer) CreateUser(ctx echo.Context, req CreateUserJSONRequestBody) error {
    // req уже валидирован по схеме OpenAPI
    // Никакой рефлексии при десериализации
    return nil
}

Плюс: контракт компилируется. Минус: потеря гибкости, сложная миграция при изменении схем.

5. Производительность и Mechanical Sympathy

Подача документации в production имеет измеримую стоимость:

  • Embed vs Disk: //go:embed компилирует файлы в секцию .rodata бинарника. Чтение происходит напрямую из памяти процесса через mmap. Задержка ~0.1 мкс. Чтение с диска или кеша прокси — 1-10 мс + syscall open/read.
  • Gzip сжатие: swagger.json часто достигает 100-500 КБ. Без сжатия передача занимает 1-3 RTT. Встроенный http.FileServer не сжимает на лету. Решение: предсжатие swagger.json.gz через gzip на этапе сборки и кастомный ReadDoc(), отдающий сжатый поток с заголовком Content-Encoding: gzip.
  • Memory Footprint: Swagger UI (HTML/JS/CSS) занимает ~5 МБ в памяти при загрузке. В кластере из 100 подов это 500 МБ суммарно. Рекомендуется отдавать UI через CDN, а в сервисе встраивать только swagger.json и минималистичный index.html (например, scalar или rapidoc вместо тяжелого swagger-ui-dist).

6. Ловушки и лучшие практики

  • Не документируйте реализации: В спецификации должны быть только стабильные контракты. Временные хаки, внутренние заголовки или X-Debug поля не попадают в openapi.yaml.
  • Версионирование в путях: Документируйте /v1/ и /v2/ отдельно. Используйте info.version и servers для указания актуального окружения.
  • Примеры ответов: @success 200 {object} UserResponse обязателен. Без примеров автогенераторы клиентов создают бесполезные null-структуры.
  • Ошибки: Документируйте @failure 400, 401, 403, 404, 429, 500 с единой структурой ErrorResponse. Это синхронизирует обработку ошибок из 15. Error handling в сервисах.

Итог

  1. В Go документация — это статический контракт, генерируемый на этапе сборки, а не динамический рантайм-отчет.
  2. Code-Fast (swag) удобен для старта, Spec-First (oapi-codegen) надежен для масштабируемых систем.
  3. Используйте //go:embed для встраивания статики, избегайте чтения файлов с диска в production.
  4. Синхронизируйте теги валидации и OpenAPI-аннотации, чтобы спецификация отражала реальную логику сервиса.
  5. Интегрируйте проверку соответствия кода и документации в CI-пайплайн через git diff.
  6. Отдавайте тяжелый UI через CDN, в бинарник встраивайте только минимальный рендерер и swagger.json.

Правильно организованная документация превращается из "дополнительной работы" в инструмент разработки: автогенерация тестов, моков, клиентских библиотек и валидация запросов на уровне шлюза.

Следующая статья: 34. Тестирование HTTP сервисов