Protobuf
Официальный сайт: Protocol Buffers | Google Developers
Введение
Официальный учебник: Protocol Buffer Basics: Go | Protocol Buffers | Google Developers
Protocol Buffers — это независимый от языка, независимый от протокола, расширяемый механизм сериализации структурированных данных, открытый Google в 2008 году. Он обеспечивает более быструю распаковку и упаковку, обычно используется в области RPC-коммуникации. Он может определять структурированный способ данных, а затем использовать специально сгенерированный исходный код для лёгкого чтения и записи структурированных данных из различных потоков данных и использования их на различных языках. Protocol Buffers в дальнейшем тексте называется protobuf.
protobuf довольно популярен, особенно в экосистеме Go, где gRPC использует его в качестве механизма сериализации для передачи протоколов.
Синтаксис
Сначала давайте посмотрим на пример, чтобы увидеть, как вообще выглядит файл protobuf. В целом, его синтаксис очень прост и может быть освоен примерно за десять минут. Ниже приведён пример файла с именем search.proto. Расширение файла для protobuf — .proto.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
}
message SearchResult {
string data = 1;
}
service SearchService {
rpc Search(SearchRequest) returns(SearchResult);
}- Первая строка
syntax = "proto3";указывает на использование синтаксисаproto3, который является значением по умолчанию. - Объявление
messageпохоже на struct и является базовой структурой вproto. SearchRequestопределяет три поля, каждое с именем и типом.serviceопределяет службу, которая содержит один или несколько RPC-интерфейсов.- RPC-интерфейсы должны иметь ровно один параметр и одно возвращаемое значение, и их типы должны быть
message, а не базовыми типами.
Кроме того, обратите внимание, что каждая строка в файле proto должна заканчиваться точкой с запятой.
Комментарии
Стиль комментариев точно такой же, как в Go.
syntax = "proto3";
/* Комментарий
* Комментарий */
message SearchRequest {
string query = 1; // Комментарий
string number = 2;
}Типы
Модификаторы типов могут появляться только в message и не могут появляться отдельно.
Базовые типы
| Тип proto | Тип Go |
|---|---|
| double | float64 |
| float | float32 |
| int32 | int32 |
| int64 | int64 |
| uint32 | uint32 |
| uint64 | uint64 |
| sint32 | int32 |
| sint64 | int64 |
| fixed32 | uint32 |
| fixed64 | uint64 |
| sfixed32 | int32 |
| sfixed64 | int64 |
| bool | bool |
| string | string |
| bytes | []byte |
Массивы
Добавление модификатора repeated перед базовым типом указывает на тип массива, соответствующий срезу в Go.
message Company {
repeated string employee = 1;
}Map
Формат определения типа map в protobuf следующий:
map<key_type, value_type> map_field = N;key_type должен быть числовым или строковым, а value_type не имеет ограничений по типу. Вот пример:
message Person {
map<string, int64> cards = 1;
}Поля
Фактически proto не является традиционным типом ключ-значение. В объявленном файле proto конкретные данные не появляются. После = каждого поля должно быть уникальное число в текущем message. Эти числа используются для идентификации и определения этих полей в двоичных сообщениях. Числа начинаются с 1, при этом числа 1-15 занимают 1 байт, а 16-2047 занимают 2 байта. Поэтому часто появляющимся полям следует присваивать числа 1-15 для экономии места, и следует зарезервировать некоторое место для полей, которые могут часто появляться в будущем.
Поля в message должны следовать этим правилам:
singular: Это тип поля по умолчанию. В хорошо структурированномmessageможет быть только 0 или 1 такое поле, что означает, что одно и то же поле не может существовать повторно. Следующее объявление вызовет ошибку.protobufsyntax = "proto3"; message SearchRequest { string query = 1; string number = 2; string number = 3; // Дубликат поля }optional: Похож наsingular, но позволяет явно проверить, установлено ли значение поля. Могут быть следующие две ситуации:set: Будет сериализованоunset: Не будет сериализовано
repeated: Этот тип поля может появляться 0 или несколько раз. Повторяющиеся значения будут сохранены в порядке (проще говоря, это массив, который позволяет одному и тому же типу значения появляться несколько раз и сохраняет их в порядке появления, что является индексом).map: Поле типа пары ключ-значение, объявляется следующим образом:protobufmap<string,int32> config = 3;
Зарезервированные поля
Ключевое слово reserve может объявлять зарезервированные поля. После объявления номера зарезервированного поля его нельзя использовать как номер и имя других полей, и компиляция также не удастся. Официальный ответ Google: если файл proto удаляет некоторые числа в новой версии, другие пользователи могут повторно использовать эти удалённые числа в будущем. Однако, если вернуться к номерам старой версии, это вызовет несоответствие между полями и их соответствующими номерами, что приведёт к ошибкам. Зарезервированные поля могут служить напоминанием во время компиляции, напоминая вам, что вы не можете использовать эти зарезервированные поля, иначе компиляция не удастся.
syntax = "proto3";
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
reserved "a"; // Объявить конкретное имя поля как зарезервированное поле
reserved 1 to 2; // Объявить последовательность чисел как зарезервированное поле
reserved 3,4; // Объявить
}С этим файл не пройдёт компиляцию.
Устаревшие поля
Если поле устарело, его можно записать следующим образом:
message Body {
string name = 1 [deprecated = true];
}Enum
Вы можете объявить константы enum и использовать их как типы полей. Обратите внимание, что первый элемент enum должен быть нулём, потому что значение по умолчанию enum — первый элемент.
syntax = "proto3";
enum Type {
GET = 0;
POST = 1;
PUT = 2;
DELETE = 3;
}
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
Type type = 5;
}Когда внутри enum есть элементы enum с одинаковым значением, вы можете использовать псевдонимы enum:
syntax = "proto3";
enum Type {
option allow_alias = true; // Нужно включить конфигурацию псевдонимов
GET = 0;
GET_ALIAS = 0; // Псевдоним для элемента enum GET
POST = 1;
PUT = 2;
DELETE = 3;
}
message SearchRequest {
string query = 1;
string number = 2;
map<string, int32> config = 3;
repeated string a = 4;
Type type = 5;
}Вложенные сообщения
message Outer { // Уровень 0
message MiddleAA { // Уровень 1
message Inner { // Уровень 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Уровень 1
message Inner { // Уровень 2
int32 ival = 1;
bool booly = 2;
}
}
}message может вкладывать объявления message, как вложенные struct.
Package
Вы можете добавить необязательный модификатор package в файлы protobuf для предотвращения конфликтов имён между типами сообщений протокола.
package foo.bar;
message Open { ... }Затем вы можете использовать имя пакета при определении полей в типах сообщений:
message Foo {
...
foo.bar.Open open = 1;
...
}Import
Импорт позволяет нескольким файлам protobuf совместно использовать определения. Синтаксис следующий, и расширения файлов нельзя опускать при импорте.
import "a/b/c.proto";Импорты используют относительные пути, но этот относительный путь не является относительным путём между импортирующим файлом и импортируемым файлом. Он зависит от пути сканирования, указанного при генерации кода компилятором protoc. Предположим, существует следующая структура файлов:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoЕсли нам нужно только сгенерировать код для части каталога player и указать только каталог player при сканировании путей, то взаимные импорты между health.proto и player.proto могут напрямую писать имена файлов. Например, player.proto импортирует health.proto:
import "health.proto";Если в этот момент player.proto импортирует файлы в каталоге common.proto или monster, компиляция не удастся, поэтому следующая запись полностью неверна, потому что компилятор не может найти эти файлы:
import "../common.proto"; // Неправильная записьTIP
Кстати, .., . эти символы не разрешены к появлению в путях импорта.
Предположим, pb_learn указан как путь сканирования во время компиляции, тогда вы можете импортировать файлы из других каталогов через относительные пути. Фактический путь импорта — это относительный адрес абсолютного адреса файла относительно pb_learn. Посмотрите на следующий пример импорта других файлов player.proto:
import "common.proto";
import "monster/monster.proto";
import "player/health.proto";Даже health.proto в том же каталоге теперь должен использовать относительные пути. Поэтому в проекте мы обычно создаём отдельную папку для хранения всех файлов protobuf и указываем её как путь сканирования во время компиляции. Все поведения импорта в этом каталоге также основаны на его относительном пути.
TIP
Если вы используете редактор GoLand, для созданных вами каталогов protobuf он не может быть разрешён по умолчанию, что приводит к красным выделениям. Чтобы GoLand распознал его, нужно вручную установить путь сканирования. Принцип точно такой же, как описано выше. Метод настройки следующий: откройте следующие настройки:
File | Settings | Languages & Frameworks | Protocol BuffersВручную добавьте путь сканирования в Import Paths, который должен совпадать с путём, указанным во время компиляции.

Any
Тип Any позволяет использовать сообщения как встроенные типы без необходимости их определений proto. Вы можете напрямую импортировать определённые Google типы, которые встроены и не нуждаются в ручном написании.
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}Google также предварительно определил много других типов. Посетите protobuf/ptypes at master · golang/protobuf (github.com), чтобы увидеть больше, в основном включая:
- Обёртки базовых типов
- Типы времени
- Типы длительности
Их определения protobuf должны быть в каталоге include компилятора protoc.
OneOf
Объяснение официальной документации здесь слишком многословно. Проще говоря, это означает, что поле может иметь несколько возможных типов во время передачи, но в конечном итоге будет использован только один тип. Его внутренность не может содержать поля, модифицированные repeated, как union в C.
message Stock {
// Данные, специфичные для акций
}
message Currency {
// Данные, специфичные для валюты
}
message ChangeNotification {
int32 id = 1;
oneof instrument {
Stock stock = 2;
Currency currency = 3;
}
}Service
Ключевое слово service может определять RPC-службу. RPC-служба содержит несколько RPC-интерфейсов, которые делятся на унарные интерфейсы и потоковые интерфейсы.
message Body {
string name = 1;
}
service ExampleService {
rpc DoSomething(Body) returns(Body);
}Потоковые интерфейсы далее делятся на однонаправленную потоковую передачу и двунаправленную потоковую передачу, обычно модифицируются ключевым словом stream. Посмотрите на следующий пример:
message Body {
string name = 1;
}
service ExampleService {
// Потоковая передача клиента
rpc DoSomething(stream Body) returns(Body);
// Потоковая передача сервера
rpc DoSomething1(Body) returns(stream Body);
// Двунаправленная потоковая передача
rpc DoSomething2(stream Body) returns(stream Body);
}Потоковая передача означает долгосрочную взаимную отправку данных в соединении, больше не как простой вопрос-ответ, как унарные интерфейсы.
Empty
Empty на самом деле является пустым message, соответствующим пустой struct в Go. Он редко используется для модификации полей и в основном используется для указания, что RPC-интерфейс не нуждается в параметрах или не имеет возвращаемого значения.
syntax = "proto3";
import "google/protobuf/empty.proto";
service EmptyService {
rpc Do(google.protobuf.Empty) returns(google.protobuf.Empty);
}Option
Options обычно используются для управления некоторыми поведениями protobuf. Например, для управления пакетом, сгенерированным для исходного кода языка Go, вы можете объявить следующим образом:
option go_package = "github/jack/sample/pb_learn;pb_learn"Перед точкой с запятой — путь импорта для других исходных файлов после генерации кода, а после точки с запятой — имя пакета для соответствующего сгенерированного файла.
Он может делать некоторые оптимизации со следующими доступными значениями, которые нельзя объявлять повторно:
SPEED: Высший уровень оптимизации, наибольший объём сгенерированного кода, это значение по умолчанию.CODE_SIZE: Уменьшает объём сгенерированного кода, но полагается на рефлексию для сериализации.LITE_RUNTIME: Наименьший объём кода, но не хватает некоторых функций.
Вот пример использования:
option optimize_for = SPEED;Кроме того, options также могут добавлять некоторые метаданные в message и enum. Вы можете получить эту информацию через рефлексию, что особенно полезно для проверки параметров.
Компиляция
Компиляция — это генерация кода. Выше мы только определили файлы protobuf. В фактическом использовании они должны быть преобразованы в исходный код конкретного языка для использования. Мы завершаем это через компилятор protoc, который поддерживает несколько языков.

Установка
Чтобы загрузить компилятор, перейдите на protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format (github.com) для загрузки последнего Release, как правило, сжатый файл:
protoc-25.1-win64
│ readme.txt
│
├─bin
│ protoc.exe
│
└─include
└─google
└─protobuf
│ any.proto
│ api.proto
│ descriptor.proto
│ duration.proto
│ empty.proto
│ field_mask.proto
│ source_context.proto
│ struct.proto
│ timestamp.proto
│ type.proto
│ wrappers.proto
│
└─compiler
plugin.protoПосле загрузки добавьте каталог bin в переменные среды для использования команды protoc. После завершения проверьте версию. Нормальный вывод указывает на успешную установку:
$ protoc --version
libprotoc 25.1Загруженный компилятор не поддерживает язык Go по умолчанию, потому что генерация кода языка Go — это отдельный исполняемый файл, в то время как другие языки объединены вместе. Поэтому установите плагин языка Go для перевода определений protobuf в исходный код языка Go:
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latestЕсли вам также нужно сгенерировать код службы gRPC, установите следующий плагин:
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latestПосле установки проверьте их версии:
$ protoc-gen-go-grpc --version
protoc-gen-go-grpc 1.3.0
$ protoc-gen-go --version
protoc-gen-go.exe v1.31.0Эти плагины также являются отдельными двоичными файлами, но могут вызываться только через protoc и не могут выполняться независимо:
(эта программа должна запускаться protoc, а не напрямую)Кроме того, есть много других плагинов, таких как плагины для генерации документации интерфейса openapi и т.д. Если интересно, вы можете поискать самостоятельно.
Генерация
Всё ещё используя предыдущий пример, структура следующая:
pb_learn
│ common.proto
│
├─monster
│ monster.proto
│
└─player
health.proto
player.protoДля генерации кода нужно указать всего три параметра:
- Путь сканирования: сообщает компилятору, где искать файлы
protobufи как разрешать пути импорта. - Путь генерации: где размещаются скомпилированные файлы.
- Целевые файлы: указывает, какие целевые файлы нужно скомпилировать.
Перед началом убедитесь, что go_package в файлах protobuf установлен правильно. Используйте protoc -h для проверки его поддерживаемых параметров. Наиболее часто используемые — -I или --proto_path, которые можно использовать несколько раз для указания нескольких путей сканирования, например:
$ protoc --proto_path=./pb_learn --proto_path=./third_partyПростого указания путей сканирования недостаточно; вам также нужно указать путь генерации и целевые файлы protobuf. Здесь мы генерируем файлы Go, поэтому используйте параметр --go_out, поддерживаемый ранее загруженным плагином protoc-gen-go:
$ cd pb_learn
$ protoc --proto_path=. --go_out=. common.proto
$ ls
common.pb.go common.proto monster/ player/Параметр --go_out указывает путь генерации. . означает текущий путь, а common.proto указывает файл для компиляции. Если вы хотите сгенерировать код grpc (предварительное условие: установлен плагин grpc), вы можете добавить параметр --go-grpc_out (если файл protobuf не определяет service, соответствующий файл не будет сгенерирован):
$ protoc --proto_path=. --go_out=. --go-grpc_out=. common.proto
$ ls
common.pb.go common.proto common_grpc.pb.go monster/ player/common.pb.go — это сгенерированное определение типа protobuf, а common_grpc.pb.go — это сгенерированный код gRPC, который основан на первом. Если определение языка не сгенерировано, код gRPC не может быть сгенерирован.
Если вы хотите скомпилировать все файлы protobuf в каталоге, вы можете использовать подстановочный знак *, например *.proto:
$ protoc --proto_path=. --go_out=. --go-grpc_out=. ./*.protoЕсли вы хотите включить все файлы, вы можете использовать подстановочный знак **, например ./**/*.proto:
$ protoc --proto_path=. --go_out=. --go-grpc_out=. ./**/*.protoОднако этот метод применим только к оболочкам, которые поддерживают этот подстановочный знак. Например, в Windows cmd или powershell не поддерживают эту запись:
D> protoc --proto_path=. --go_out=. --go-grpc_out=. ./**/*.proto
Неверный шаблон имени файла или отсутствующий входной файл "./**/*.proto"К счастью, gitbash поддерживает многие команды Linux и также может заставить Windows поддерживать этот синтаксис. Чтобы избежать написания повторяющихся команд каждый раз, вы можете поместить их в makefile:
.PHONY: all
proto_gen:
protoc --proto_path=. \
--go_out=paths=source_relative:. \
--go-grpc_out=paths=source_relative:. \
./**/*.proto ./*.protoВы можете заметить, что добавлено paths=source_relative:, которое устанавливает режим пути генерации файла. Существуют следующие опции:
paths=import: Это значение по умолчанию. Файлы генерируются в каталоге, указанномimport. Это также может быть путь модуля. Например, если есть файлprotos/buzz.protoи вы указываетеpaths=example.com/project/protos/fizz, он в конечном итоге сгенерируетexample.com/project/protos/fizz/buzz.pb.go.module=$PREFIX: Во время генрации префикс пути будет удалён. В приведённом выше примере, если вы указываете префиксexample.com/project, он в конечном итоге сгенерируетprotos/fizz/buzz.pb.go. Этот режим в основном используется для генерации непосредственно в модуле (ощущается, как будто нет разницы).paths=source_relative: Сгенерированные файлы поддерживают ту же относительную структуру, что и файлыprotobufв указанном каталоге.
После двоеточия : указывается путь генерации:
| common.proto
| common.pb.go
│
├─monster
│ monster.pb.go
│ monster.proto
│
└─player
health.pb.go
health.proto
health_grpc.pb.go
player.pb.go
player.protoРефлексия
Вы можете расширить enum и message через options. Сначала импортируйте "google/protobuf/descriptor.proto":
import "google/protobuf/descriptor.proto";
extend google.protobuf.EnumValueOptions {
optional string string_name = 123456789;
}
enum Integer {
INT64 = 0[
(string_name) = "int_64"
];
}Это эквивалентно добавлению метаданных к значению enum. То же самое применимо к message:
import "google/protobuf/descriptor.proto";
extend google.protobuf.MessageOptions {
optional string my_option = 51234;
}
message MyMessage {
option (my_option) = "Hello world!";
}Это эквивалентно наличию рефлексии о protobuf. После генерации кода вы можете получить доступ через Descriptor:
func main() {
message := pb_learn.MyMessage{}
message.ProtoReflect().Descriptor().Options().ProtoReflect().Range(func(descriptor protoreflect.FieldDescriptor, value protoreflect.Value) bool {
fmt.Println(descriptor.FullName(), ":", value)
return true
})
}Вывод:
my_option:"Hello world!"Этот подход можно сравнить с добавлением тегов к struct в Go; они ощущаются похожими. На основе этого подхода вы также можете реализовать функциональность проверки параметров. Вам просто нужно написать правила в options и проверить через Descriptor.
