Содержание урока · 11 разделов
- Proto-комментарии: единый источник правды
- protoc-gen-doc: страницы документации из buf generate
- Дескрипторы: схема как данные
- Рефлексия: сервер рассказывает о себе
- OpenAPI: контракт для внешнего мира
- Инструменты документации и рефлексии
- Правила proto-комментариев, которые работают
- Что дальше по курсу
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
К этому уроку у вас собран полный buf-конвейер: lint из десятого урока, генерация из одиннадцатого, публикация модулей в BSR из двенадцатого. Не хватает одной ступени — документации, и мы добавим её туда же, в buf.gen.yaml, чтобы docs обновлялись тем же коммитом, что и код. Принцип урока один: документация protobuf — это не отдельный артефакт, который кто-то обязан поддерживать руками, а производная схемы. Если описание метода живёт в вики, оно разъедется с кодом за месяц; если тот же текст лежит комментарием над rpc в .proto, из него собираются сразу четыре вещи: doc-комментарии в сгенерированном Go, markdown-страницы от protoc-gen-doc, descriptions в OpenAPI-контракте и ответы gRPC-рефлексии живому grpcurl-у. Один источник, четыре потребителя, ноль ручного переноса. Вторая тема урока — service-рефлексия: способность сервера рассказать о своих сервисах по запросу клиента. С ней grpcurl и buf curl работают против вашего API без единого локального файла — как swagger UI для RPC-мира. С ней же связан риск: рефлексия раскрывает поверхность API, и на проде её либо защищают перехватчиками из тринадцатого урока, либо выключают. Начнём с самого нижнего слоя — с комментариев в схеме — и поднимемся до OpenAPI-контракта для внешних потребителей.
Proto-комментарии: единый источник правды#
Комментарии в proto-файле — обычные C-стилевые строки: двойной слэш перед объявлением. Правило простое, как в Go: комментарий над объявлением service, rpc, message или поля становится документацией этого элемента во всех генератах. protoc-gen-go переносит его doc-комментарием на соответствующий тип и метод — комментарий над rpc попадает на интерфейс вашего хендлера и на клиентский метод, и IDE показывает его при каждом наведении. Комментарий над полем доезжает до struct-поля сгенерированного сообщения. Никакой магии: это текст, который protoc хранит в дескрипторах рядом с именами методов и полей, а плагины решают, что с ним делать. Отсюда первый рабочий вывод — комментарии пишутся для потребителя API, а не для автора: «GetNote возвращает заметку по идентификатору» бесполезно, когда имя метода и так это говорит; полезно объяснить семантику — что вернётся, если заметки нет, чьи заметки видны, какие идентификаторы валидны. Второй вывод — linter может держать планку: в buf есть отдельная категория правил COMMENTS (COMMENT_SERVICE, COMMENT_RPC, COMMENT_FIELD и соседние), которые требуют комментария у каждого сервиса, метода и поля; в дефолтный набор они не входят, их включают в buf.yaml вручную — и с тех пор PR без комментариев не проходит проверку. Третий вывод: формат комментариев стоит стандартизировать в команде — например, первая строка как краткое summary, дальше абзацы деталей, а пометки об экспериментальности и сроках удаления — отдельным тегом. Всё это обычный текст, но дисциплина в комментариях — это то, что отличает схему, по которой можно работать, от схемы, которую приходится раскапывать в исходниках хендлера.
Схема, которая документирует себя сама
GO// NoteService управляет заметками пользователя.
service NoteService {
// GetNote возвращает заметку по идентификатору.
// Заметка другого пользователя отвечает CodeNotFound:
// существование чужих заметок не раскрываем.
rpc GetNote(GetNoteRequest) returns (GetNoteResponse);
// CreateNote создаёт заметку. Текст ограничен 10 000 символов
// и проверяется на стороне сервера.
rpc CreateNote(CreateNoteRequest) returns (CreateNoteResponse);
}
message GetNoteRequest {
// Уникальный идентификатор заметки, например "n_01H...".
string id = 1;
}Комментарий над service описывает сервис целиком; комментарий над rpc — контракт метода, включая поведение с чужими заметками; комментарий над полем объясняет формат значения. Эти же строки позже окажутся в Go-коде, markdown-доках, OpenAPI и ответах рефлексии.
Где в Connect-проекте должен жить текст, описывающий контракт метода API?
Где вы увидите эти комментарии в Go-коде?
Прочитайте код и выберите, что он выведет:
// NoteService управляет заметками пользователя.
service NoteService {
// GetNote возвращает заметку по идентификатору.
rpc GetNote(GetNoteRequest) returns (GetNoteResponse);
}
// protoc-gen-go отработал по этой схеме и выдал gen/go/notes/v1/protoc-gen-doc: страницы документации из buf generate#
Самый известный генератор документации protobuf — protoc-gen-doc: он читает дескрипторы схемы и её комментарии и выдаёт markdown, HTML, docbook или JSON. Настраивается как обычный плагин в buf.gen.yaml из одиннадцатого урока — либо локально установленным бинарником, либо удалённым плагином BSR buf.build/community/pseudomuto-doc, которому не нужен ничего в вашем окружении. Порядок работы не отличается от кодогенерации: buf generate читает схему, гоняет список плагинов, каждый пишет свои файлы в свой out-каталог. Доки едут в репозиторий рядом с кодом, и это не компромисс, а выигрыш: изменения документации ревьюятся в том же PR, что и изменения схемы, история — в git, а CI из одиннадцатого урока, который гоняет buf generate и проверяет git diff --exit-code, заодно не даёт докам отстать от схемы — забыл перегенерировать, тест не пройдёт. Дальше с артефактом можно делать что угодно: коммитить markdown в папку docs, публиковать HTML на внутренней странице, собирать из JSON собственный портал. Тот же проток документа помогает BSR-модулям из двенадцатого урока: в веб-интерфейсе Buf README модуля и описания элементов видны потребителям, а источник у них всё тот же — комментарии схемы. Единственная ловушка плагина — шаблоны: дефолтный markdown-шаблон выводит всё подряд, и на больших схемах документ превращается в простыню; protoc-gen-doc поддерживает кастомные шаблоны, где вы решаете, что попадёт в короткую справку, а что в полный справочник. Но это уже тюнинг — базовая связка buf generate плюс protoc-gen-doc закрывает потребность в доков для внутреннего API целиком.
Дескрипторы: схема как данные#
Под всеми генераторами лежит одна структура — FileDescriptorSet: компилированное представление proto-файлов, где описаны сообщения, поля, сервисы, методы и — важно для этой темы — комментарии с координатами строк. Получить её можно двумя командами, которые вы уже знаете: buf build -o image.binpb пишет буф-образ, который и есть тот же дескриптор-набор, а классический protoc --descriptor_set_out=out.binpb --include_imports собирает то же самое с импортами. Флаг --include_imports существен: без него в наборе не будет зависимостей вроде well-known-типов, и инструменты, которым нужен полный граф, споткнутся. Дальше начинается самое интересное: сгенерированный Go-код несёт дескриптор в себе. В каждом сгенерированном пакете лежит переменная вида File_notes_v1_note_service_proto — скомпилированный дескриптор файла, из которого всё сгенерировано. Через пакет google.golang.org/protobuf/reflect/protoreflect по этому дескриптору можно пройтись как по данным: перечислить сервисы, у сервиса методы, у метода вход и выход, у файла — комментарии. Именно поэтому рефлексия, генераторы и валидаторы работают без исходных proto-файлов: схема уже внутри вашего бинарника. Практические применения этого факта шире доков: динамический маршрутизатор, который регистрирует хендлеры по списку методов из дескриптора; служебный эндпоинт, отдающий JSON со списком версий API; проверка, что собранный сервис содержит те методы, о которых заявляет деплой. Buf breaking из десятого урока сравнивает дескрипторы старой и новой схемы — тот же механизм, поднятый на уровень CI. Так что когда слышите «схема — это данные», это не лозунг: FileDescriptorSet — обычный protobuf, который можно хранить, сравнивать, публиковать и читать из кода.
Дескриптор в руки: CLI и protoreflect
GO// CLI: собрать полный дескриптор-набор с импортами
buf build -o image.binpb
protoc -I proto --include_imports \
--descriptor_set_out=out.binpb proto/notes/v1/notes.proto
// Go: дескриптор уже лежит в сгенерированном пакете
import "google.golang.org/protobuf/reflect/protoreflect"
func listMethods(file protoreflect.FileDescriptor) []string {
names := []string{}
services := file.Services()
for i := 0; i < services.Len(); i++ {
methods := services.Get(i).Methods()
for j := 0; j < methods.Len(); j++ {
names = append(names, string(methods.Get(j).Name()))
}
}
return names
}
// listMethods(notesv1.File_notes_v1_note_service_proto) -> [GetNote CreateNote ...]buf build и protoc --descriptor_set_out дают схему как файл, сгенерированная переменная File_notes_v1_note_service_proto — как значение в памяти. protoreflect — стандартный способ читать сервисы и методы без исходников.
Что такое FileDescriptorSet и зачем он нужен, если proto-файлы и так лежат в репозитории?
Рефлексия: сервер рассказывает о себе#
Service-рефлексия — протокольная возможность, унаследованная у gRPC: клиент спрашивает сервер «какие у тебя сервисы и методы», и сервер отвечает дескрипторами. Для классического gRPC за это отвечает пакет google.golang.org/grpc/reflection, а grpcurl без флагов -proto сначала стучится в рефлексию. В мире Connect ту же возможность даёт пакет connectrpc.com/grpcreflect: его рефлектор раздаёт дескрипторы ваших сервисов через обычные хендлеры, которые монтируются на тот же mux, что и бизнес-API, — и grpcurl, и buf curl начинают работать против вашего сервера без локальных proto-файлов. Пакет свежий, точное API сверяйте с README, но форма устойчивая: рефлектор конструируется со списком файлов, которые он готов отдавать, и монтируется двумя хендлерами — стабильной версией протокола и ранней alpha, которую слушают текущие grpcurl и buf curl. Список файлов — это те самые сгенерированные переменные File_..._proto из прошлого раздела: вы явно перечисляете, о чём сервер готов рассказывать. Зачем это нужно на практике. Первое — исследовать API: grpcurl list, grpcurl describe — и у новичка в команде вместо просьбы «скинь proto» есть живой источник с актуальным состоянием. Второе — отладка: buf curl против рефлексии отвечает на вопрос «а что сервер реально отдаёт в проде», включая версии методов, которые могли измениться после последнего деплоя. Третье — инструменты: универсальные клиенты и генераторы прокси могут строить запросы по ответу рефлексии. Цена вопроса — поверхность атаки: рефлексия раскрывает всё, что вы ей скормили, включая сервисы, которые вы считали внутренними. Поэтому правило тринадцатого урока продолжается и здесь: на публичном граничном сервере рефлексия либо выключена, либо закрыта авторизацией, либо отдаёт урезанный список файлов.
Монтируем рефлексию рядом с бизнес-API
GOimport (
"connectrpc.com/grpcreflect"
notesv1 "yourorg/notes/gen/go/notes/v1"
)
func newMux() *http.ServeMux {
mux := http.NewServeMux()
// Бизнес-API: как в третьем уроке
path, handler := notesv1connect.NewNoteServiceHandler(¬esServer{})
mux.Handle(path, handler)
// Рефлексия: grpcurl и buf curl видят сервисы без proto-файлов
reflector := grpcreflect.NewReflector(
grpcreflect.WithFiles(notesv1.File_notes_v1_note_service_proto),
)
mux.Handle(grpcreflect.NewHandlerV1(reflector))
mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector))
return mux
}
// Проверка: grpcurl -plaintext localhost:8080 listРефлектору перечисляются файлы, о которых он рассказывает, — здесь только бизнес-схема, ничего лишнего. Два хендлера закрывают версии протокола рефлексии, которые слушают grpcurl и buf curl. Точное имя опций пакета сверьте с README — API ещё молод.
На прод-сервере смонтировали grpcreflect со списком всех файлов проекта, включая экспериментальные сервисы. Чем это плохо?
OpenAPI: контракт для внешнего мира#
Proto-схема и рефлексия покрывают внутренний мир, где все понимают RPC. Но у внешнего мира свой язык: Swagger UI, шлюзы, генераторы SDK для языков, в которых proto-инструментария нет, и люди, которым «контракт — это открытый yaml, а не бинарник». Ответ на это — OpenAPI, и для proto-схем он тоже генерируется: главный генератор — protoc-gen-openapiv2 из grpc-gateway, который выдаёт OpenAPI 2.0 (Swagger) прямо из дескрипторов; есть и protoc-gen-openapi из gnostic с OpenAPI 3. Описания в контракте берутся из тех же комментариев схемы — у вас уже всё написано, генератор только разложит текст по полям summary и description. HTTP-маршруты описываются аннотациями google/api/annotations — это те самые googleapis-зависимости из двенадцатого урока: вы объявляете, что GetNote живёт на GET /notes/v1/{id}, и OpenAPI получает настоящий путь с параметрами, а не абстрактный rpc. Для Connect-проекта связка openapi с connect-сервисом — это мост к браузерным клиентам из девятого урока: фронтенд, который ходит через HTTP-шлюз или читает контракты в Swagger UI, получает привычный артефакт; а дружелюбие Connect-протокола к curl из четвёртого урока означает, что примеры из OpenAPI можно исполнять curl-ом буквально. Честная оговорка: protoc-gen-openapiv2 исторически описывает grpc-gateway-маршруты, а не connect-эндпоинты — для чисто connect-трафика путь выглядит как /notes.v1.NoteService/GetNote, и его в контракт дописывают шаблоном плагина или вручную. Поэтому практический порядок такой: OpenAPI — для потребителей за границей вашего RPC-мира (шлюзы, фронтенд-команды, публичный API), а proto и рефлексия — для тех, кто работает с API напрямую. Два артефакта из одного источника, и расхождение между ними физически невозможно.
Полный buf.gen.yaml: код, доки, OpenAPI
GOversion: v2
plugins:
- local: protoc-gen-go
out: gen/go
opt: paths=source_relative
- local: protoc-gen-connect-go
out: gen/go
opt: paths=source_relative
- remote: buf.build/community/pseudomuto-doc
out: docs/
opt: markdown,notes.md
- local: protoc-gen-openapiv2
out: openapi/
opt: allow_merge=true,merge_file_name=notesПервые два плагина — генерация Go-кода из одиннадцатого урока; третий пишет markdown-доки из комментариев; четвёртый собирает OpenAPI-контракт. Один buf generate обновляет всё: код, доки и контракт едут в один PR и не могут разъехаться.
Инструменты документации и рефлексии#
| Инструмент | Вход | Выход | Роль в проекте |
|---|---|---|---|
| комментарии в .proto | ваши руки | текст в дескрипторах | единственный источник правды для всего ниже |
| protoc-gen-doc | дескрипторы схемы | markdown, HTML, JSON | человекочитаемые доки на каждый PR |
| protoc-gen-openapiv2 | дескрипторы + аннотации | OpenAPI 2.0 | контракт для шлюзов, SDK и внешнего мира |
| buf build -o | модуль buf | образ (descriptor set) | схема как артефакт: хранить, сравнивать, публиковать |
| grpcreflect | сгенерированные File-переменные | эндпоинты рефлексии | сервер сам рассказывает о сервисах |
| grpcurl / buf curl | рефлексия или --schema | вызовы и describe | исследование и отладка живого API |
Правила proto-комментариев, которые работают#
- Пишите для потребителя: семантика, гарантии, ошибки — не пересказ имени метода
- Первая строка комментария — краткое summary, дальше абзацы деталей
- Описывайте ошибки: какой код и когда вернётся, — это читают и в IDE, и в OpenAPI
- Включите buf-категорию COMMENTS: PR без комментариев не проходит lint
- Экспериментальные методы помечайте тегом и сроком — и удаляйте вовремя
- Перегенерация доков — обязанность того же PR, что и изменение схемы; CI с git diff --exit-code следит
Соберите buf.gen.yaml с доками и OpenAPI
Восстановите конфиг генерации: к двум плагинам Go-кода добавлены документационный и OpenAPI-плагины.
Путь комментария: от rpc до потребителя
Расставьте шаги пути одного комментария над rpc — от написания до того, как его увидит внешний потребитель.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Сгенерированный каталог — не место для правок: buf generate сотрёт их следующим запуском. Рукописная документация рядом с генерируемой — второй способ нажить расхождение: если что-то нельзя выразить комментарием схемы, это признак проблемы схемы, а не повод для отдельного документа. Исключение — README с инструкцией по сборке и запуску: он про процесс, а не про контракт.
Проверяйте себя простым вопросом: может ли потребитель по одному тексту комментария написать корректный клиент? Если в комментарии нет поведения при отсутствии объекта, лимитов и кодов ошибок — контракт неполный. buf lint с правилами COMMENTS держит дисциплину наличия, а ревью — качества: на схему смотрите как на публичное API, потому что она им и является.
Код урока — настоящий Go и настоящий yaml: buf.gen.yaml дописывается в проект из одиннадцатого урока, а рефлексия монтируется на хендлер из третьего. Браузерная песочница сайта buf и protoc-плагины не исполняет, поэтому исполняемых блоков в курсе нет — интерактивы работают на странице и дают XP, а живую проверку делайте grpcurl-ом против локального сервера.
Карточки: документация и рефлексия
Что дальше по курсу#
Документация замкнулась на buf-конвейер: схема описывает контракт словами, генераторы разносят его по потребителям, рефлексия отвечает на вопросы живого сервера. Дальше по курсу: шестнадцатый урок — тестирование, где эта же схема станет предметом тестов полного стека на httptest — шестнадцатый; метрики семнадцатого покажут, как документированный API ведёт себя под нагрузкой. Для повторения: конвейер генерации — в одиннадцатом уроке, lint и breaking changes — в десятом, зависимость googleapis — в двенадцатом. Программа всех двадцати пяти уроков — на странице курса.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- Документация protobuf-схемы — производная, а не отдельный труд: единственный источник — комментарии в .proto, всё остальное генерируется.
- Комментарий над rpc доезжает в четыре места: doc-комментарий Go, markdown от protoc-gen-doc, description в OpenAPI, ответы рефлексии.
- buf lint умеет требовать комментарии: категория правил COMMENTS включается в конфиге, PR без документации не проходит.
- FileDescriptorSet — схема как данные: buf build -o и protoc --descriptor_set_out дают файл, сгенерированный пакет несёт дескриптор в себе; protoreflect читает его из кода.
- connectrpc.com/grpcreflect поднимает gRPC-рефлексию на том же mux: grpcurl и buf curl работают без proto-файлов; на проде рефлексию защищают или сужают список файлов.
- OpenAPI генерируется из тех же дескрипторов (protoc-gen-openapiv2, OpenAPI 3 — через gnostic): контракт для шлюзов, Swagger UI и внешних SDK, маршруты — через google/api/annotations.
- Один buf.gen.yaml собирает код, доки и контракт одним запуском: расхождение артефактов физически невозможно, CI с git diff --exit-code держит их свежими.
Домашнее задание
Проведите аудит документации своего notes-сервиса и достройте конвейер. Шаг первый: включите в buf.yaml категорию правил COMMENTS, запустите buf lint и допишите комментарии ко всем сервисам, методам и полям, на которые он пожалуется, — для каждого метода опишите поведение при отсутствии объекта и возвращаемые коды ошибок. Шаг второй: добавьте в buf.gen.yaml плагины pseudomuto-doc и protoc-gen-openapiv2, перегенерируйте и убедитесь, что в docs/ появился markdown с вашим текстом, а в openapi/ — контракт; закоммитьте всё одним PR. Шаг третий: смонтируйте grpcreflect рядом с бизнес-хендлером, запустите grpcurl list и grpcurl describe против локального сервера, затем вызовите метод через buf curl без --schema — рефлексия должна дать схему. Шаг четвёртый: попробуйте откомментировать экспериментальный сервис из списка рефлексии и решите, что на публичном контуре вы с ним делаете — закрываете перехватчиком из тринадцатого урока или убираете из WithFiles. Запишите в заметки три строки «артефакт — генератор — потребитель»: она станет чеклистом документации в следующем проекте.
Личные заметкиПусто
Вопросы и ответы по уроку
Зачем OpenAPI, если уже есть proto и рефлексия?
Потому что внешний мир читает OpenAPI: Swagger UI, HTTP-шлюзы и генераторы SDK для языков без proto-инструментария живут в этом формате. Генерация из тех же дескрипторов даёт два артефакта из одного источника — для RPC-потребителей остаётся proto, для HTTP-мира OpenAPI, и расхождение невозможно.
protoc-gen-openapiv2 генерирует пути grpc-gateway. А connect-эндпоинты?
Исторически плагин описывает gateway-маршруты из аннотаций; чистый connect-путь вида /notes.v1.NoteService/GetNote в контракт дописывают шаблоном плагина или постобработкой. На практике этого достаточно: контракт нужен внешним потребителям за шлюзом, а прямые RPC-клиенты работают по proto и рефлексии.
Как не дать документации устареть?
Убрать возможность устареть: комментарии живут в схеме, доки генерируются тем же buf generate, что и код, и коммитятся тем же PR. CI из одиннадцатого урока с buf generate и git diff --exit-code не пропустит изменение схемы без перегенерации — доки отстать не могут.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Сравните с альтернативами
Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.
Изучайте дальше
Следующая ступень учебного пути
Fyne: десктопные и мобильные GUI на Go
Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.
- Создавать окна и интерфейсы из виджетов
- Управлять компоновкой: Box, Grid, Form
- Рисовать примитивами canvas и строить свои виджеты
- Связывать данные через binding