Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 21 из 25 ~20 мин +50 XP

REST-шлюз и HTTP-совместимость

rest шлюз grpc — классическая задача: proto-бэкенд нужен REST-клиентам, у которых нет protobuf-рантайма. Разберём два пути: grpc-gateway с аннотациями google.api.http — отдельный генератор и реверс-прокси перед сервером, и врождённая HTTP-совместимость Connect — JSON поверх того же хендлера, GET-запросы для процедур без побочных эффектов и ошибки, уже замапленные на HTTP-статусы.

Содержание урока · 10 разделов

Рано или поздно к proto-сервису приходит клиент, который не умеет protobuf: curl в шпаргалке дежурного, внешний монитор, партнёр с python-requests, браузерная закладка. Классический ответ экосистемы gRPC — rest шлюз grpc: отдельный компонент, принимающий обычный HTTP и JSON и транслирующий их в gRPC-вызовы. Эталонная реализация — grpc-gateway: кодогенерация из аннотаций google.api.http и реверс-прокси рядом с сервером; в поиске его ищут и как grpc gateway без дефиса — проект один и тот же. Connect отвечает на ту же задачу иначе: HTTP-натура встроена в сам протокол — тот же хендлер из третьего урока принимает JSON POST от любого HTTP-клиента, ошибки из шестого урока уже замаплены на статусы, а GET-запросы для процедур без побочных эффектов включаются одной опцией proto. В этом уроке соберём оба пути руками, сравним их по кодогенерации, деплою, стримам и ошибкам — и сформулируем, когда gateway всё ещё правильный выбор, а когда connect rest закрывает вопрос без нового компонента в архитектуре.

grpc-gateway: прокси из аннотаций#

Механика grpc-gateway: в proto рядом с rpc пишется аннотация google.api.http — какой HTTP-метод и путь соответствует процедуре и как поля сообщения раскладываются по пути, query и телу. Генератор protoc-gen-grpc-gateway выдаёт runtime-код: http-роутер, который принимает HTTP-запрос с JSON, собирает из него protobuf-сообщение и делает gRPC-вызов к бэкенду. То есть gateway — полноценный реверс-прокси: свой процесс или sidecar, свой порт, свои таймауты, своя схема ошибок, которую надо трансформировать в HTTP-статусы и не дать ей расползтись с реальностью бэкенда. Практическая деталь: gateway-роутер можно держать и в том же процессе, что и gRPC-сервер — mux соседствует с grpc-сервером через отдельные порты, но это не отменяет второй кодогенерации и второй схемы ошибок. Бонусы честные: точный контроль REST-контракта — красивые пути вроде /v1/notes/42, кастомные префиксы, версии в пути; и генерация swagger/OpenAPI из тех же аннотаций для внешних партнёров. Цена тоже честная: второй стек кодогенерации в CI, второй артефакт в деплое, лишний хоп латентности, стримы gateway не проксирует — только unary, и двойная схема ошибок, которую надо синхронизировать руками. Это инструмент слоя перевода, и использовать его стоит тогда, когда слой перевода действительно нужен.

Аннотация google.api.http

GO
import "google/api/annotations.proto";

service NoteService {
  rpc GetNote(GetNoteRequest) returns (GetNoteResponse) {
    option (google.api.http) = {
      get: "/v1/notes/{note_id}"
      additional_bindings {
        get: "/v1/notes"
      }
    };
  }
}

Классика gateway: GET /v1/notes/42 маршрутизируется в GetNote, поле note_id берётся из пути, вторая привязка отдаёт список. Для создания — post с телом, для удаления — delete. Аннотации живут в proto, поэтому REST-контракт версионируется вместе со схемой, а buf lint из десятого урока проверяет импорт annotations.proto.

Что физически представляет собой grpc-gateway в деплое?

connect rest: JSON из коробки#

Connect переворачивает задачу: шлюз не нужен, потому что HTTP — родная среда протокола. connect rest — это когда тот же хендлер принимает обычный POST с JSON: путь процедуры /notes.v1.NoteService/GetNote, тело — сообщение в protojson, ответ — JSON обратно. curl-однострочник вместо клиента, монитор без рантайма, быстрый дебаг на проде — без единой аннотации и без прокси. Ошибки шестого урока уже замаплены на HTTP-статусы: not_found приедет 404-м, unauthenticated — 401-м, resource_exhausted — 429-м, с JSON-телом, где код и сообщение читаются глазами. Браузерный клиент из девятого урока говорит тем же протоколом — connect-web ходит прямо на хендлер, CORS настраивается middleware из девятнадцатого урока; gRPC-шлюзу для браузера понадобился бы ещё и grpc-web-прокси. protojson-нюансы из восемнадцатого урока в силе: int64 и 64-битные целые — строки, enum — строковые имена, camelCase по умолчанию, json_name переименовывает поля в JSON. Отдадим должное: путей вида /v1/notes/42 здесь нет — пути процедурные; это плата за отсутствие аннотаций, и о компенсациях ниже. Ещё одна приятность жанра: запрос можно повторить той же командой из истории shell — идемпотентные чтения не создают дублей, а ответ виден прямо в терминале вместе со статусом; дежурный решает инцидент, не разворачивая клиентов. Платформенный бонус: тот же JSON-жанр слушают мониторы и smoke-тесты пайплайна — им не нужен protobuf-рантайм, только curl и адрес.

curl к хендлеру без шлюза

GO
curl -s http://localhost:8080/notes.v1.NoteService/GetNote \
  -H "Content-Type: application/json" \
  -d '{"noteId": "42"}'

# ответ:
# {"note": {"noteId": "42", "title": "Первые шаги", "tags": ["go"]}}

Connect-жанр: POST на процедурный путь, JSON по protojson-правилам, ответ — JSON с полями в camelCase. Ошибка приедет статусом 404 и телом с кодом not_found — ничего генерировать не нужно. Content-Type и путь определяют жанр: те же правила, что разбирал четвёртый урок.

GET-запросы и кэширование#

POST на чтение режет кэширование: браузер, CDN и обратный прокси по умолчанию не кэшируют POST. Connect даёт управляемый ответ: в proto у процедуры ставится option idempotency_level = NO_SIDE_EFFECTS — и для Connect-жанра клиент начинает слать GET с сообщением, закодированным в query-строке, а хендлер принимает его без дополнительных флагов: генерация, клиент и сервер читают опцию из дескриптора. GetNote становится честным GET /notes.v1.NoteService/GetNote?noteId=42 — HTTP-кэши между клиентом и сервером работают по стандартным правилам, повторная отправка безопасна по определению. Ограничения умные. Первое: работает только Connect-жанр — gRPC и gRPC-Web в принципе не знают GET-вызовов и продолжают POST. Второе: query-кодирование накладывает размерные пределы URL, поэтому NO_SIDE_EFFECTS ставят на лёгкие read-процедуры с простыми полями, а не на запросы с большими фильтрами и вложенными структурами. Третье: идемпотентность — семантическое заявление, а не магия; PutNote с естественной идемпотентностью можно пометить тоже, если повтор возвращает тот же результат. Это противовес аннотациям gateway: не кастомный путь, а стандартный процедурный GET с кэшируемостью.

NO_SIDE_EFFECTS и GET-вызов

GO
// proto/notes/v1/notes.proto
rpc GetNote(GetNoteRequest) returns (GetNoteResponse) {
  option idempotency_level = NO_SIDE_EFFECTS;
}

// вызов без клиента и без тела:
curl -s "http://localhost:8080/notes.v1.NoteService/GetNote?noteId=42"

Пометка ставится в proto, кодогенерация не меняется, connect-клиент из пятого урока шлёт GET автоматически, а хендлер декодирует query в сообщение. Если кэширование входит в план — выставляй Cache-Control сознательно: хендлер сам заголовков кэша не добавляет, стандартное поведение HTTP оставляет решение тебе.

Что увидит middleware на сервере?

Прочитайте код и выберите, что он выведет:

// proto: option idempotency_level = NO_SIDE_EFFECTS на GetNote
res, err := client.GetNote(ctx,
        connect.NewRequest(&notesv1.GetNoteRequest{NoteId: "42"}))
// вопрос: что зафиксирует withLogging из девятнадцатого урока?

JSON-канон и ошибки на HTTP#

JSON-слой Connect — не своя выдумка, а protojson-канон protobuf: camelCase-имена по умолчанию, json_name из восемнадцатого урока переименовывает поле для JSON, 64-битные целые — строки, потому что JavaScript не умеет точные int64, enum — строковые имена, bytes — base64, отсутствующее поле — отсутствие ключа, а не null. REST-клиент на python-requests читает ответ без сюрпризов, если его автор помнит про int64-строки; ошибка приходит HTTP-статусом и JSON-телом с кодом — те же коды, что gRPC-клиент видит в заголовках grpc-status. Сравнение с gateway честное: у gateway трансляция ошибок настраивается руками и часто расползается с реальностью бэкенда после рефакторингов; у Connect это один код-путь в хендлере, который обслуживает все жанры сразу. curl-режим делает JSON-слой ещё и инструментом диагностики: любой ответ читается глазами без декодера, и тот же вызов, что в шпаргалке дежурного, служит контрактом для монитора. Если нужны документы API — пятнадцатый урок показывал генерацию OpenAPI из дескрипторов, без аннотаций. Отдельный жанр задач — отладка чужих интеграций: партнёр присылает скриншот ответа, и JSON-канон позволяет понять глазами, что поле пришло строкой, а не числом, без декодера и без repro-скрипта; в gRPC-мире для того же понадобился бы дамп трафика с декодированием.

grpc-gateway против Connect#

Критерийgrpc-gatewayConnect
Кодогенерациявторой генератор protoc-gen-grpc-gateway и runtimeтот же плагин connect-go, файлы рядом
Деплойотдельный процесс или sidecar, второй порттот же хендлер и порт, ноль новых компонентов
Путикастомные REST: /v1/notes/{note_id}процедурные: /notes.v1.NoteService/GetNote
GET и кэшаннотации http.getidempotency_level = NO_SIDE_EFFECTS
Стримыне проксируются, только unaryработают все три вида из восьмого урока
Ошибкисвои правила трансляции, держать вручнуюкоды в статусы из коробки, один источник
БраузерHTTP JSON из коробкиconnect-web из девятого урока, CORS на mux
Документы APIswagger из аннотацийOpenAPI из дескрипторов, пятнадцатый урок

Когда gateway всё ещё выбирают#

Честный ответ на «зачем тогда grpc-gateway»: когда внешний REST-контракт уже существует и его нельзя менять. Партнёрская интеграция со спекой GET /v1/notes/{id}?include=tags, платформенная политика «публичные API только в /v1», OpenAPI-документ, сгенерированный из аннотаций и розданный клиентам, — в этих случаях gateway легален и полезен: он ставит слой перевода между стабильным REST-контрактом и живущей своей жизнью proto-схемой. Второй легальный случай — тяжёлые требования к путям и методам, которые опциями proto не выразить. Гибрид тоже рабочий: connect-хендлер на общем mux из девятнадцатого урока обслуживает RPC и браузерных клиентов, а gateway смонтирован рядом на те две-три процедуры, у которых есть внешний REST-контракт; либо ещё проще — пара обычных http.HandlerFunc-роутов рядом с RPC, если внешний контракт тривиален, с r.PathValue для сегментов пути. Такие соседи-роуты живут под тем же TLS и тем же логированием, что и RPC, и не требуют нового ingress-правила. Антипаттерн один: тянуть gateway ради «чтобы был», когда реальных REST-клиентов ноль — тогда это второй артефакт, второй порт и вторая схема ошибок без единой пользы.

Сервис живёт на Connect, внешних REST-контрактов нет, партнёры могут качать SDK. Нужен ли grpc-gateway, чтобы curl мог вызвать GetNote?

Включи GET для процедуры

Помети GetNote как запрос без побочных эффектов, чтобы connect-клиент слал GET.

rpc GetNote(GetNoteRequest) returns (GetNoteResponse) { option = ; } // вызов без тела: // GET /notes.v1.NoteService/GetNote?noteId=42

Путь GET-вызова

Расставь путь вызова GetNote от connect-клиента до ответа при NO_SIDE_EFFECTS.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Карточки: REST поверх proto

Термин

Что такое grpc-gateway

нажмите, чтобы перевернуть
Определение

Реверс-прокси из кодогенерации: аннотации google.api.http в proto, protoc-gen-grpc-gateway, HTTP-сервер с JSON, транслирующий вызовы в gRPC. Свой процесс, порт и схема ошибок.

1 / 6

Что дальше по курсу#

HTTP-поверхность закрыта с двух сторон: из коробки — процедурные JSON-вызовы и GET-кэширование, по требованию — gateway под внешний контракт. Дальше — деплой: Docker, ingress, HTTP/2 на границе и TLS, где JSON-поверхность и gRPC-жанры встречаются с реальным трафиком. Для повторения: жанры и маршрутизация по Content-Type — в четвёртом уроке, ошибки и статусы — в шестом, браузерный клиент и CORS — в девятом, json_name и совместимость схемы — в восемнадцатом, mux и http.HandlerFunc-соседи — в девятнадцатом. Программа всех двадцати пяти уроков — на странице курса.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • rest шлюз grpc в классике — grpc-gateway: аннотации google.api.http, protoc-gen-grpc-gateway, реверс-прокси с JSON перед gRPC-бэкендом.
  • connect rest не требует шлюза: тот же хендлер принимает JSON POST на процедурный путь, protojson-канон, ошибки уже статусы.
  • idempotency_level = NO_SIDE_EFFECTS включает GET с query для Connect-жанра — HTTP-кэши работают; gRPC-жанр всегда POST.
  • protojson: camelCase, json_name, int64-строки, enum-имена, base64 для bytes, отсутствующее поле не сериализуется.
  • Gateway легален под существующий внешний REST-контракт: кастомные пути, OpenAPI из аннотаций, платформенная политика.
  • Стримы gateway не проксирует; connect-хендлер стримит из коробки — браузерному клиенту прокси не нужен.
  • Тривиальный внешний GET — обычные http.HandlerFunc рядом с RPC на общем mux; шлюз — слой перевода, а не украшение.
  • Браузерная граница: connect-web ходит прямо на хендлер с CORS-middleware из девятого урока, grpc-web-прокси не нужен.

Домашнее задание

Собери обе HTTP-поверхности и сравни их руками. Шаг первый: к notes-сервису из третьего урока добавь import annotations.proto и аннотацию google.api.http для GetNote, включи protoc-gen-grpc-gateway в кодогенерацию, подними gateway рядом и проверь curl-ом GET /v1/notes/42 — путь, query и ошибку 404 при неизвестном идентификаторе. Шаг второй: та же процедура через Connect — curl с POST JSON на процедурный путь, затем поставь option idempotency_level = NO_SIDE_EFFECTS и повтори вызов GET-ом с query; зафиксируй, что код клиента из пятого урока не изменился ни на строку. Шаг третий: сравни ошибки — добей not_found в обоих путях, сравни тело JSON и то, что видит gRPC-клиент в заголовках; выпиши, где схема расползается, а где один источник. Шаг четвёртый: замерь латентность двух хопов gateway против одного хопа connect-хендлера на сотне вызовов curl-ом. Шаг пятый: напиши один http.HandlerFunc-сосед /v1/notes/{id} с r.PathValue на общем mux из девятнадцатого урока и реши честно: нужен ли тебе gateway как слой перевода или хватает JSON-жанра и пары HandlerFunc — запиши вывод и критерии в заметки проекта.

Личные заметкиПусто
Хранятся только в вашем браузере (Connect · REST-шлюз и HTTP-совместимость)

Вопросы и ответы по уроку

Connect заменяет grpc-gateway полностью?

Только там, где не нужен кастомный REST-контракт. Процедурные пути и NO_SIDE_EFFECTS закрывают внутренние, мониторинговые и мобильные потребности; под партнёрскую спеку с путями /v1 и розданным OpenAPI gateway остаётся честным слоем перевода между стабильным контрактом и живущей своей жизнью схемой.

Как отдавать Swagger или OpenAPI?

Два источника правды: из аннотаций gateway — генератор openapiv2, из proto-дескрипторов — конвейер из пятнадцатого урока без аннотаций. Выбирай один и держи документы в CI, чтобы они не отстали от схемы; расползшийся OpenAPI хуже отсутствующего.

Можно ли комбинировать оба пути?

Да, на общем mux из девятнадцатого урока: connect-хендлер для RPC и браузерных клиентов, gateway или пара HandlerFunc для внешних REST-путей. Главное — один источник истины по ошибкам и статусам: пусть REST-слой не выдумывает свои коды, а наследует их из имплементации.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

Следующая ступень учебного пути

Fyne25 уроков~13 чпрактика25 уроков уже доступны

Fyne: десктопные и мобильные GUI на Go

Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.

  • Создавать окна и интерфейсы из виджетов
  • Управлять компоновкой: Box, Grid, Form
  • Рисовать примитивами canvas и строить свои виджеты
  • Связывать данные через binding