Основы Protocol Buffer: Go
Базовое введение в работу с protocol buffers для программистов на Go.
Это руководство предоставляет базовое введение для программистов на Go в работу с protocol buffers, используя proto3 версию языка protocol buffers. На примере создания простого приложения оно показывает, как:
- Определять форматы сообщений в файле
.proto. - Использовать компилятор protocol buffer.
- Использовать Go API protocol buffer для записи и чтения сообщений.
Это не исчерпывающее руководство по использованию protocol buffers в Go. Для получения более подробной справочной информации см. Руководство по языку Protocol Buffer, Справочник по Go API, Руководство по сгенерированному коду на Go и Справочник по кодированию.
Проблемная область
Пример, который мы будем использовать, — это очень простое приложение "адресная книга", которое может читать и записывать контактные данные людей в файл и из файла. Каждый человек в адресной книге имеет имя, ID, адрес электронной почты и контактный номер телефона.
Как сериализовать и извлекать такие структурированные данные? Есть несколько способов решить эту проблему:
- Использовать gobs для сериализации структур данных Go. Это хорошее решение в специфичной для Go среде, но оно не работает хорошо, если вам нужно делиться данными с приложениями, написанными для других платформ.
- Вы можете придумать специальный способ кодирования элементов данных в единую строку — например, кодируя 4 целых числа как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания одноразового кода кодирования и разбора, и разбор накладывает небольшие затраты времени выполнения. Это лучше всего работает для кодирования очень простых данных.
- Сериализовать данные в XML. Этот подход может быть очень привлекательным, поскольку XML (вроде как) читаем человеком и есть библиотеки привязок для множества языков. Это может быть хорошим выбором, если вы хотите делиться данными с другими приложениями/проектами. Однако XML печально известен своей прожорливостью к месту, и кодирование/декодирование может наложить огромные штрафы на производительность приложений. Кроме того, навигация по DOM-дереву XML значительно сложнее, чем навигация по простым полям в структуре в обычных условиях.
Protocol buffers — это гибкое, эффективное, автоматизированное решение для
решения именно этой проблемы. С protocol buffers вы пишете .proto описание
структуры данных, которую хотите сохранить. На основе этого компилятор protocol buffer
создает класс, который реализует автоматическое кодирование и разбор данных protocol buffer
в эффективном бинарном формате. Сгенерированный класс предоставляет
геттеры и сеттеры для полей, составляющих protocol buffer, и заботится
о деталях чтения и записи protocol buffer как единого целого.
Важно, что формат protocol buffer поддерживает идею расширения формата со временем таким образом,
что код все еще может читать данные, закодированные в старом формате.
Где найти пример кода
Наш пример — это набор приложений командной строки для управления файлом данных
адресной книги, закодированным с использованием protocol buffers. Команда add_person_go добавляет
новую запись в файл данных. Команда list_people_go разбирает файл данных
и печатает данные в консоль.
Вы можете найти полный пример в директории examples репозитория GitHub.
Определение вашего формата протокола
Чтобы создать ваше приложение "адресная книга", вам нужно начать с файла .proto.
Определения в файле .proto просты: вы добавляете сообщение для
каждой структуры данных, которую хотите сериализовать, затем указываете имя и тип для
каждого поля в сообщении. В нашем примере файл .proto, который определяет
сообщения, это
addressbook.proto.
Файл .proto начинается с объявления пакета, которое помогает предотвратить
конфликты имен между разными проектами.
syntax = "proto3";
package tutorial;
import "google/protobuf/timestamp.proto";
Опция go_package определяет путь импорта пакета, который
будет содержать весь сгенерированный код для этого файла. Имя пакета Go будет
последним компонентом пути импорта. Например, наш пример будет использовать
имя пакета "tutorialpb".
option go_package = "github.com/protocolbuffers/protobuf/examples/go/tutorialpb";
Далее идут ваши определения сообщений. Сообщение — это просто агрегат,
содержащий набор типизированных полей. Многие стандартные простые типы данных
доступны в качестве типов полей, включая bool, int32, float, double и string. Вы
также можете добавить дальнейшую структуру в ваши сообщения, используя другие типы сообщений в качестве
типов полей.
message Person {
string name = 1;
int32 id = 2; // Уникальный ID номер для этого человека.
string email = 3;
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
repeated PhoneNumber phones = 4;
google.protobuf.Timestamp last_updated = 5;
}
enum PhoneType {
PHONE_TYPE_UNSPECIFIED = 0;
PHONE_TYPE_MOBILE = 1;
PHONE_TYPE_HOME = 2;
PHONE_TYPE_WORK = 3;
}
// Наш файл адресной книги - это просто один из таких.
message AddressBook {
repeated Person people = 1;
}
В приведенном выше примере сообщение Person содержит сообщения PhoneNumber,
в то время как сообщение AddressBook содержит сообщения Person. Вы даже можете определять
типы сообщений, вложенные внутрь других сообщений — как вы можете видеть, тип PhoneNumber
определен внутри Person. Вы также можете определить типы enum, если хотите,
чтобы одно из ваших полей имело одно из предопределенного списка значений — здесь вы хотите
указать, что номер телефона может быть одним из PHONE_TYPE_MOBILE,
PHONE_TYPE_HOME или PHONE_TYPE_WORK.
Маркеры " = 1", " = 2" на каждом элементе идентифицируют уникальный "тег", который поле использует в бинарном кодировании. Номера тегов 1-15 требуют на один байт меньше для кодирования, чем более высокие номера, поэтому в качестве оптимизации вы можете решить использовать эти теги для часто используемых или повторяющихся элементов, оставляя теги 16 и выше для реже используемых необязательных элементов. Каждый элемент в повторяющемся поле требует перекодировки номера тега, поэтому повторяющиеся поля являются особенно хорошими кандидатами для этой оптимизации.
Если значение поля не установлено, используется значение по умолчанию: ноль для числовых типов, пустая строка для строк, false для bool. Для встроенных сообщений значением по умолчанию всегда является "экземпляр по умолчанию" или "прототип" сообщения, у которого none (ни одно) из его полей не установлено. Вызов метода доступа для получения значения поля, которое не было явно установлено, всегда возвращает значение по умолчанию для этого поля.
Если поле repeated (повторяющееся), поле может повторяться любое количество раз
(включая ноль). Порядок повторяющихся значений будет сохранен в
protocol buffer. Думайте о повторяющихся полях как о динамически sized arrays (массивах переменного размера).
Вы найдете полное руководство по написанию файлов .proto — включая все
возможные типы полей — в
Руководстве по языку Protocol Buffer.
Однако не ищите возможности, похожие на наследование классов — protocol
buffers этого не делают.
Компиляция ваших Protocol Buffers
Теперь, когда у вас есть .proto, следующее, что нужно сделать, — это сгенерировать
классы, которые вам понадобятся для чтения и записи сообщений AddressBook
(и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить компилятор
protocol buffer protoc на вашем .proto:
-
Если вы не установили компилятор, скачайте пакет и следуйте инструкциям в README.
-
Выполните следующую команду, чтобы установить плагин Go protocol buffers:
go install google.golang.org/protobuf/cmd/protoc-gen-go@latestПлагин компилятора
protoc-gen-goбудет установлен в$GOBIN, по умолчанию в$GOPATH/bin. Он должен быть в вашем$PATH, чтобы компилятор протоколаprotocмог его найти. -
Теперь запустите компилятор, указав исходную директорию (где находится исходный код вашего приложения — используется текущая директория, если вы не предоставили значение), целевую директорию (куда вы хотите, чтобы сгенерированный код попал; часто та же, что и
$SRC_DIR) и путь к вашему.proto. В этом случае вы бы вызвали:protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.protoПоскольку вы хотите код Go, вы используете опцию
--go_out— похожие опции предоставляются для других поддерживаемых языков.
Это генерирует
github.com/protocolbuffers/protobuf/examples/go/tutorialpb/addressbook.pb.go
в указанной вами целевой директории.
API Protocol Buffer
Генерация addressbook.pb.go дает вам следующие полезные типы:
- Структуру
AddressBookс полемPeople. - Структуру
Personс полями дляName,Id,EmailиPhones. - Структуру
Person_PhoneNumberс полями дляNumberиType. - Тип
Person_PhoneTypeи значение, определенное для каждого значения в перечисленииPerson.PhoneType.
Вы можете прочитать больше о деталях того, что именно генерируется, в Руководстве по сгенерированному коду на Go, но в основном вы можете рассматривать их как совершенно обычные типы Go.
Вот пример из
модульных тестов команды list_people
того, как вы можете создать экземпляр Person:
p := pb.Person{
Id: 1234,
Name: "John Doe",
Email: "jdoe@example.com",
Phones: []*pb.Person_PhoneNumber{
{Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
},
}
Написание сообщения
Вся цель использования protocol buffers — сериализовать ваши данные так, чтобы их
можно было разобрать в другом месте. В Go вы используете функцию Marshal
из библиотеки proto
(Marshal)
для сериализации ваших данных protocol buffer. Указатель на struct сообщения protocol buffer
реализует интерфейс proto.Message. Вызов
proto.Marshal возвращает protocol buffer, закодированный в его wire format (бинарном формате).
Например, мы используем эту функцию в
команде add_person:
book := &pb.AddressBook{}
// ...
// Записываем новую адресную книгу обратно на диск.
out, err := proto.Marshal(book)
if err != nil {
log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
log.Fatalln("Failed to write address book:", err)
}
Чтение сообщения
Чтобы разобрать закодированное сообщение, вы используете функцию Unmarshal
из библиотеки proto
(Unmarshal).
Вызов этого разбирает данные в in как protocol buffer и помещает
результат в book. Таким образом, чтобы разобрать файл в
команде list_people,
мы используем:
// Читаем существующую адресную книгу.
in, err := ioutil.ReadFile(fname)
if err != nil {
log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
log.Fatalln("Failed to parse address book:", err)
}
Расширение Protocol Buffer
Рано или поздно после того, как вы выпустите код, использующий ваш protocol buffer, вы несомненно захотите "улучшить" определение protocol buffer. Если вы хотите, чтобы ваши новые буферы были обратно совместимы, а ваши старые буферы были вперед совместимы — а вы почти certainly (несомненно) хотите этого — тогда есть некоторые правила, которым вы должны следовать. В новой версии protocol buffer:
- вы не должны изменять номера тегов любых существующих полей.
- вы можете удалять поля.
- вы можете добавлять новые поля, но вы должны использовать новые номера тегов (т.е. номера тегов, которые никогда не использовались в этом protocol buffer, даже удаленными полями).
(Есть некоторые исключения из этих правил, но они редко используются.)
Если вы следуете этим правилам, старый код будет happily (с радостью) читать новые сообщения и просто игнорировать любые новые поля. Для старого кода, сингулярные поля, которые были удалены, будут просто иметь свое значение по умолчанию, а удаленные повторяющиеся поля будут пусты. Новый код также будет прозрачно читать старые сообщения.
Однако имейте в виду, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет сделать что-то разумное со значением по умолчанию. Используется зависящее от типа значение по умолчанию: для строк значением по умолчанию является пустая строка. Для булевых значений значением по умолчанию является false. Для числовых типов значением по умолчанию является ноль.