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

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

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

Урок 2 из 25 ~22 мин +50 XP

Protocol Buffers: синтаксис proto3

Контракт gRPC-сервиса начинается с .proto-файла, и proto3 синтаксис — это вся грамматика, которая вам понадобится: message, номера полей, enum, oneof, map и service. Разберём каждый элемент на схеме сервиса заказов и посмотрим, во что превращается каждый тип в Go.

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

Команда проектирует сервис заказов: две команды, два репозитория, и в каждом своя копия структуры Order на Go, которые дрейфуют врозь с каждым спринтом. Это ровно та проблема, для которой придуман .proto-файл: структура описывается один раз, лежит в одном месте, а код для обеих сторон генерируется. В этом уроке пройдём весь синтаксис proto3, который встречается в боевых схемах: анатомию файла, поля и их номера, скалярные типы и их соответствие Go, повторяющиеся поля, enum, oneof и объявление сервиса с методами. Файлы получаются короткими — обычно сотня-две строк на сервис, — но в них умещается несколько неочевидных правил, и почти все они связаны с одним: .proto описывает не структуру данных в памяти, а способ её передачи по сети. Именно поэтому у полей есть номера, у enum обязателен ноль, а у схемы есть правила эволюции, о которых в конце урока.

Анатомия .proto-файла: syntax, package, go_package#

Каждый файл начинается с объявления версии: syntax = "proto3"; — это должна быть первая непустая строка, иначе protoc примет файл за старый proto2 и начнёт жаловаться. Дальше package — пространство имён: одинаковые имена сообщений в разных пакетах не конфликтуют, а главное, package попадает в путь HTTP-вызова: метод CreateOrder сервиса OrderService из пакета shop.v1 будет доступен по пути /shop.v1.OrderService/CreateOrder. Затем опция go_package — она задаёт import-путь пакета, в который ляжет сгенерированный Go-код; после точки с запятой можно указать алиас имени пакета, если путь заканчивается неудобно. Схемы импортируют друг друга через import, чаще всего — встроенные google/protobuf/timestamp.proto или google/protobuf/empty.proto. Имя файла — lower_snake_case: order_service.proto, а не OrderService.proto. Один файл держите одной темой: заказы в одном, платежи в другом — конфликты слияний будут благодарны.

message: поля, типы и номера#

message — это тип данных, грубый аналог struct в Go: у каждого поля есть тип, имя и номер. Номера — не украшение, а основа формата: в бинарном виде по проводам едут пары «номер поля + значение», а имена — нигде. Именно поэтому правило номер один звучит так: номер поля после релиза менять нельзя. Поле с номером от 1 до 15 кодируется одним байтом, где номер и тип поля упакованы вместе; номера от 16 до 2047 требуют уже двух байтов. На маленьких сообщениях, которых миллионы, это заметно, поэтому сложившаяся практика — отдавать первые номера самым горячим полям: идентификаторам, статусам, суммам. Гнаться за этим с линейкой не стоит, но знать правило полезно. Типы полей — скалярные из таблицы ниже, другие message или enum; вложенные message объявляются прямо внутри родителя, если тип больше нигде не нужен.

Скалярные типы proto3 и их типы в Go#

Тип в proto3Тип в GoКогда брать
stringstringтекст, идентификаторы, UTF-8
bytes[]byteдвоичные данные, сырые файлы
int32 / int64int32 / int64целые; 64 бита для сумм и времён в секундах
uint32 / uint64uint32 / uint64счётчики, размеры
sint32 / sint64int32 / int64частые отрицательные числа: кодируется компактнее int
boolboolфлаги
float / doublefloat32 / float64научные значения; для денег не подходит
enumименованный тип int32статусы, категории
messageуказатель на структурувложенные объекты, timestamp, duration

repeated, optional и map#

Три модификатора покрывают почти всё остальное. repeated перед типом означает список: repeated OrderItem items = 2; превращается в Go-поле []OrderItem. map<K, V> даёт словарь map[K]V, но с ограничениями: ключи — только целые или string, повторяющиеся карты запрещены, а порядок элементов не гарантирован. Самое тонкое — третий случай: по умолчанию proto3 не различает «поле равно нулю» и «поле не задано» — для скаляров оба состояния выглядят одинаково, и в Go поле имеет тип значения, а не указателя. Если различие важно (промокод «пустая строка» versus «промокода нет»), ставьте слово optional перед типом: поддержка появилась в protoc 3.15, и такие поля генерируются в Go как указатели (*string), с методами has-проверки. В моих схемах optional встречается редко — чаще я считаю отсутствие и ноль одним и тем же, но для полей, где семантика «не задано» существует по бизнес-логике, это правильный инструмент.

Схема заказа целиком

GO
syntax = "proto3";

package shop.v1;

option go_package = "github.com/example/shop/gen/shopv1";

import "google/protobuf/timestamp.proto";

message Order {
  int64 id = 1;
  repeated OrderItem items = 2;
  map<string, string> metadata = 3;
  optional string promo_code = 4;
  OrderStatus status = 5;
  google.protobuf.Timestamp created_at = 6;
}

message OrderItem {
  string sku = 1;
  int32 quantity = 2;
}

enum OrderStatus {
  ORDER_STATUS_UNSPECIFIED = 0;
  ORDER_STATUS_NEW = 1;
  ORDER_STATUS_PAID = 2;
  ORDER_STATUS_SHIPPED = 3;
  ORDER_STATUS_CANCELLED = 4;
}

В Go это превратится в структуры Order с полями Id int64, Items []OrderItem, Metadata map[string]string, PromoCode string, Status OrderStatus и CreatedAt timestamppb.Timestamp. Обратите внимание на Timestamp: это импортированный message из стандартной библиотеки protobuf, а не скалярный тип — и он же станет указателем в Go.

enum: почему ноль обязателен#

enum объявляет перечисление именованных значений, и в proto3 у него жёсткое правило: первое значение обязано быть нулём. Причина практическая, а не эстетика: ноль — значение по умолчанию для каждого нового сообщения, и если сервер пришлёт поле, которого в старой версии схемы не было, клиент увидит именно его. Поэтому конвенцией стало первое имя вида ORDER_STATUS_UNSPECIFIED — «статус не указан» — на нуле, а осмысленные статусы начиная с единицы. В Go enum становится именованным типом на базе int32 с константами вида OrderStatus_ORDER_STATUS_NEW: префикс имени типа повторяется, к этому стоит привыкнуть. Ещё один нюанс, который ловит новичков: если новая версия сервера пришлёт значение, которого нет в вашей старой схеме, Go вернёт исходное число без паники — клиентские свитчи по enum должны иметь ветку default.

oneof: ровно одно из многих

GO
message Payment {
  oneof method {
    CardPayment card = 1;
    BankTransfer bank = 2;
    CashOnDelivery cash = 3;
  }
}

message CardPayment {
  string last4 = 1;
}

message BankTransfer {
  string account = 1;
}

message CashOnDelivery {}

oneof — tagged union: одновременно задано не больше одного поля из перечисленных. В Go появится интерфейс Payment_Method с методами-маркерами isPayment_Method, а чтение удобно делать через GetCard(), GetBank(), GetCash() — невыбранное поле вернёт nil. Присвоение нового значения автоматически стирает предыдущее.

oneof и service: методы в схеме#

Зачем нужен oneof, если можно завести три optional-поля? Семантика: три optional-поля допускают состояние «заданы все три сразу», oneof — нет, и это проверяется на уровне библиотеки, а не код-ревью. Типовые сценарии — способы оплаты, фильтры поиска (ровно один критерий), варианты событий в стриме. Вторая половина урока — service: объявление сервиса собирает методы, и каждому rpc нужен тип сообщения и на входе, и на выходе: передать голый скаляр нельзя, оборачивайте в message, даже если метод ничего не принимает. Слово stream перед типом делает соответствующую сторону стриминговой: его нет — unary, слева — клиентский стрим, справа — серверный, с обеих сторон — двунаправленный. Имя сервиса вместе с package формирует путь вызова, поэтому переименовать сервис после релиза — то же самое, что сломать все клиенты разом.

Сервис заказов: методы и стриминг

GO
service OrderService {
  rpc CreateOrder (CreateOrderRequest) returns (CreateOrderReply);
  rpc GetOrder (GetOrderRequest) returns (Order);
  rpc WatchOrders (WatchRequest) returns (stream OrderEvent);
}

message CreateOrderRequest {
  repeated OrderItem items = 1;
}

message CreateOrderReply {
  int64 id = 1;
}

message GetOrderRequest {
  int64 id = 1;
}

message WatchRequest {
  OrderStatus status = 1;
}

message OrderEvent {
  Order order = 1;
  string kind = 2;
}

CreateOrder и GetOrder — unary: по одному сообщению в обе стороны. WatchOrders возвращает stream OrderEvent — это server streaming: один запрос, поток событий в ответ. Просмотрите суффиксы Request/Reply: оборачивать даже одиночные аргументы в message — обязательное правило proto3.

Проверка: номера полей

Какие номера полей protobuf кодируются самым коротким — однобайтовым — тегом?

Проверка: enum

Почему первое значение enum в proto3 обязано быть нулём?

Что напечатает код со свежим сообщением?

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

// orderpb — сгенерированный пакет из Order выше
order := &orderpb.Order{}

fmt.Println(order.Id, order.Status, order.PromoCode, len(order.Items))

Восстановите схему

Впишите пропущенные ключевые слова proto3: модификаторы полей, объявление перечисления и стриминг.

message Order { int64 id = 1; OrderItem items = 2; <string, string> labels = 3; string promo_code = 4; OrderStatus status = 5; } OrderStatus { ORDER_STATUS_UNSPECIFIED = 0; ORDER_STATUS_NEW = 1; } service OrderService { rpc CreateOrder (CreateOrderRequest) returns (CreateOrderReply); rpc WatchOrders (WatchRequest) returns ( OrderEvent); }

Соберите скелет .proto-файла

Расставьте элементы в том порядке, в котором их читает protoc — от версии синтаксиса до методов.

Строки

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

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

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

Схема готова — теперь её нужно превратить в Go-код, и для этого нужен protoc: установка плагинов, флаги и анатомия сгенерированных файлов — тема следующего урока. Сразу за ним — первый gRPC-сервер, где эта же схема заказов заработает вживую. Когда базовый синтаксис осядет, пригодятся два урока на продвинутом уровне: оптимизация сериализации и версионирование схем — оба показывают, как менять .proto, не ломая работающих клиентов.

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

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

Вывод

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

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

  • Каркас файла: syntax = "proto3", package (часть пути вызова), option go_package (куда генерировать Go-код), import для внешних схем.
  • Поля кодируются номерами: 1–15 — однобайтовый тег; после релиза номер менять нельзя.
  • repeated — срез, map — словарь без гарантии порядка, optional — указатель в Go и различие «не задано» от нуля.
  • Enum обязан начинаться с нуля — это значение по умолчанию и защита совместимости; первый элемент называют ..._UNSPECIFIED.
  • oneof — tagged union: одно значение из группы, невыбранные геттеры возвращают nil.
  • Методы service принимают и возвращают только message; слово stream слева/справа задаёт вид стриминга.
  • Правила эволюции: не менять номера и типы, удалённые поля объявлять reserved.

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

Возьмите схему заметок из домашнего задания первого урока и доведите её до полного синтаксиса: message Note с полем created_at через google/protobuf/timestamp.proto, enum NoteStatus с UNSPECIFIED-нулем, список тегов через repeated, словарь метаданных через map и oneof для фильтра поиска (по статусу или по тегу). Проверьте себя: у каждого message поля есть номер, у enum первый элемент — ноль, у каждого rpc — типы сообщений на вход и выход.

Личные заметкиПусто
Хранятся только в вашем браузере (gRPC · Protocol Buffers: синтаксис proto3)

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

Чем proto3 отличается от proto2?

proto3 убрал обязательные поля (required), добавил map и first-class стриминг, а скаляры по умолчанию не различают «нуль» и «не задано» — presence вернули позже словом optional. Для новых gRPC-сервисов используйте proto3.

Что означает UNSPECIFIED в enum?

Конвенцию ввёл Google style guide: первое нулевое значение называют ..._UNSPECIFIED и трактуют как «поле не заполнено». Это делает нулевое значение осмысленным вместо случайного статуса NEW или PAID.

Можно ли передавать деньги как float?

Нет: бинарная арифметика с плавающей точкой теряет копейки. Для денег храните сумму в int64 в минимальной единице (копейках) либо используйте тип google.type.Money из стандартной библиотеки protobuf.

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

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

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

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

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

Connect25 уроков~12 чпродвинутый25 уроков уже доступны

Connect: современный RPC от Buf

Курс Connect (Buf): протоколы, клиенты, ошибки, interceptors, браузерные клиенты, buf CLI и BSR. 25 уроков и итоговый проект API.

  • Строить сервисы на connectrpc.com/connect
  • Понимать три протокола и выбирать правильный
  • Настраивать buf: lint, breaking changes, generate
  • Писать interceptors и обрабатывать ошибки