Рано или поздно к 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
GOimport "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 к хендлеру без шлюза
GOcurl -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(¬esv1.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-gateway | Connect |
|---|---|---|
| Кодогенерация | второй генератор protoc-gen-grpc-gateway и runtime | тот же плагин connect-go, файлы рядом |
| Деплой | отдельный процесс или sidecar, второй порт | тот же хендлер и порт, ноль новых компонентов |
| Пути | кастомные REST: /v1/notes/{note_id} | процедурные: /notes.v1.NoteService/GetNote |
| GET и кэш | аннотации http.get | idempotency_level = NO_SIDE_EFFECTS |
| Стримы | не проксируются, только unary | работают все три вида из восьмого урока |
| Ошибки | свои правила трансляции, держать вручную | коды в статусы из коробки, один источник |
| Браузер | HTTP JSON из коробки | connect-web из девятого урока, CORS на mux |
| Документы API | swagger из аннотаций | 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.
Путь GET-вызова
Расставь путь вызова GetNote от connect-клиента до ответа при NO_SIDE_EFFECTS.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Query-кодирование ограничено длиной URL и простотой полей: большие фильтры, глубокие вложенные структуры и сотни repeated-элементов не поедут в строке запроса — оставь их POST. Не ставь NO_SIDE_EFFECTS на процедуры с побочными эффектами ради кэша: прокси честно поверит опции и отдаст кэшированный ответ, повторные списания и дублирующие создания — твоя ответственность. И помни: GET-режим знает только Connect-жанр, gRPC-клиенты всегда POST.
Если внешний мир просит два-три простых GET, не заводи ни gateway, ни аннотации: обычные http.HandlerFunc рядом с RPC на общем mux из девятнадцатого урока закрывают вопрос стандартной библиотекой. Шлюз оправдан, когда внешний REST-контракт большой, версионированный и живёт по своим правилам — тогда он слой перевода, а не украшение архитектуры.
Код урока — настоящий proto и настоящий shell: аннотация, NO_SIDE_EFFECTS и curl-вызовы воспроизводятся в проекте из третьего урока один в один. Браузерная песочница сетевых вызовов не исполняет, поэтому исполняемых блоков в курсе нет — интерактивы работают на странице и дают XP, а HTTP-поверхность проверяй curl-ом на локальном порту.
Карточки: REST поверх proto
Что дальше по курсу#
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 заменяет grpc-gateway полностью?
Только там, где не нужен кастомный REST-контракт. Процедурные пути и NO_SIDE_EFFECTS закрывают внутренние, мониторинговые и мобильные потребности; под партнёрскую спеку с путями /v1 и розданным OpenAPI gateway остаётся честным слоем перевода между стабильным контрактом и живущей своей жизнью схемой.
Как отдавать Swagger или OpenAPI?
Два источника правды: из аннотаций gateway — генератор openapiv2, из proto-дескрипторов — конвейер из пятнадцатого урока без аннотаций. Выбирай один и держи документы в CI, чтобы они не отстали от схемы; расползшийся OpenAPI хуже отсутствующего.
Можно ли комбинировать оба пути?
Да, на общем mux из девятнадцатого урока: connect-хендлер для RPC и браузерных клиентов, gateway или пара HandlerFunc для внешних REST-путей. Главное — один источник истины по ошибкам и статусам: пусть REST-слой не выдумывает свои коды, а наследует их из имплементации.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Fyne: десктопные и мобильные GUI на Go
Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Создавать окна и интерфейсы из виджетов
- Управлять компоновкой: Box, Grid, Form
- Рисовать примитивами canvas и строить свои виджеты
- Связывать данные через binding