суббота, 3 апреля 2021 г.

Основы работы с Protocol Buffer в Golang

Это руководство представляет собой базовое введение в работу с protocol buffer в Golang с использованием версии proto3 языка protocol buffer.

Зачем использовать protocol buffer?

Пример, который мы собираемся использовать, представляет собой очень простое приложение "адресной книги", которое может считывать и записывать контактные данные людей в файл и из файла. У каждого человека в адресной книге есть имя, идентификатор, адрес электронной почты и номер контактного телефона.

Как вы сериализуете и извлекаете подобные структурированные данные? Есть несколько способов решить эту проблему:

  • Используйте gobs для сериализации структур данных Go. Это хорошее решение для среды Go, но оно не работает, если вам нужно обмениваться данными с приложениями, написанными для других платформ.
  • Вы можете изобрести специальный способ кодирования элементов данных в одну строку - например, кодирование 4 целых чисел как "12:3:-23:67". Это простой и гибкий подход, хотя он требует написания разового кода кодирования и анализа, а анализ требует небольших затрат времени выполнения. Это лучше всего подходит для кодирования очень простых данных.
  • Сериализуйте данные в XML. Этот подход может быть очень привлекательным, поскольку XML (отчасти) удобочитаем, а библиотеки для работы с XML существуют для многих языков. Это может быть хорошим выбором, если вы хотите поделиться данными с другими приложениями/проектами. Однако, как известно, XML занимает много места, и его кодирование/декодирование может значительно снизить производительность приложений. Кроме того, навигация по дереву XML DOM значительно сложнее, чем обычная навигация по простым полям в классе.

Protocol buffer - это гибкое, эффективное, автоматизированное решение для решения именно этой проблемы. С помощью protocol buffer вы пишете .proto-описание структуры данных, которую хотите сохранить. Исходя из этого, компилятор protocol buffer создает класс, который реализует автоматическое кодирование и анализ данных protocol buffer с эффективным двоичным форматом. Сгенерированный класс предоставляет геттеры и сеттеры для полей, составляющих protocol buffer, и заботится о деталях чтения и записи protocol buffer как отдельной единицы. Важно отметить, что формат protocol buffer поддерживает идею расширения формата с течением времени таким образом, чтобы код все еще мог читать данные, закодированные в старом формате.

Пример кода

Наш пример представляет собой набор приложений командной строки для управления файлом данных адресной книги, закодированным с использованием protocol buffer. Команда add_person_go добавляет новую запись в файл данных. Команда list_people_go анализирует файл данных и выводит данные на консоль.

Вы можете найти полный пример в каталоге примеров репозитория 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;  // Уникальный идентификационный номер этого человека.
  string email = 3;

  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Наш файл адресной книги - только один из них.
message AddressBook {
  repeated Person people = 1;
}

В приведенном выше примере сообщение Person содержит сообщения PhoneNumber, а сообщение AddressBook содержит сообщения Person. Вы даже можете определять типы сообщений, вложенные в другие сообщения - как видите, тип PhoneNumber определяется внутри Person. Вы также можете определить типы перечисления (enum), если вы хотите, чтобы одно из ваших полей имело одно из предопределенного списка значений - здесь вы хотите указать, что номер телефона может быть одним из MOBILE, HOME или WORK.

Маркеры " = 1", " = 2" на каждом элементе определяют уникальный "тег", который поле использует в двоичной кодировке. Номера тегов 1-15 требуют для кодирования на один байт меньше, чем более высокие числа, поэтому в целях оптимизации вы можете решить использовать эти теги для часто используемых или повторяющихся элементов, оставив теги 16 и выше для менее часто используемых дополнительных элементов. Каждый элемент в повторяющемся поле требует перекодирования номера тега, поэтому повторяющиеся поля являются особенно хорошими кандидатами для этой оптимизации.

Если значение поля не задано, используется значение по умолчанию: ноль для числовых типов, пустая строка для строк, false для bool. Для встроенных сообщений значением по умолчанию всегда является "экземпляр по умолчанию" или "прототип" сообщения, для которого не задано ни одно из полей. Вызов метода доступа для получения значения поля, которое не было явно задано, всегда возвращает значение этого поля по умолчанию.

Если поле repeated (повторяющееся), поле может повторяться любое количество раз (включая ноль). Порядок повторения значений будет сохранен в protocol buffer. Думайте о повторяющихся полях как о массивах динамического размера.

Вы найдете полное руководство по написанию файлов .proto, включая все возможные типы полей, в Руководстве по языку protocol buffer. Однако не ищите средства, подобные наследованию классов - protocol buffer этого не делают.

Компиляция вашего protocol buffer

Теперь, когда у вас есть .proto, следующее, что вам нужно сделать, это сгенерировать классы, которые вам понадобятся для чтения и записи сообщений AddressBook (и, следовательно, Person и PhoneNumber). Для этого вам нужно запустить protocol buffer компилятор protoc на вашем .proto:

1. Если вы не установили компилятор protoc, время это сделать.

2. Выполните следующую команду, чтобы установить Go protocol buffers плагин:

go install google.golang.org/protobuf/cmd/protoc-gen-go

Плагин компилятора protoc-gen-go будет установлен в $GOBIN, по умолчанию в $GOPATH/bin. Он должен быть в вашем $PATH, чтобы компилятор protoc нашел его. Обновите свой PATH, чтобы компилятор протоколов мог найти плагин:

$ export PATH="$PATH:$(go env GOPATH)/bin"

3. Теперь запустите компилятор, указав исходный каталог (где находится исходный код вашего приложения - текущий каталог используется, если вы не указали значение), целевой каталог (где вы хотите, чтобы сгенерированный код перемещался; часто то же самое, что и $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 в указанном вами целевом каталоге.

Protocol Buffer API

Создание addressbook.pb.go дает вам следующие полезные типы:

  • Структура AddressBook с полем People.
  • Структура Person с полями для Name, Id, Email и Phones.
  • Структура Person_PhoneNumber с полями для Number и Type.
  • Тип Person_PhoneType и значение, определенное для каждого значения в перечислении Person.PhoneType.

Вы можете подробнее узнать о том, что именно генерируется в руководстве Go Generated Code, но по большей части вы можете рассматривать их как совершенно обычные типы 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.Person_HOME},
        },
}

Запись сообщения

Вся цель использования protocol buffer - сериализовать ваши данные, чтобы их можно было проанализировать где-нибудь еще. В Go вы используете функцию Marshal из библиотеки proto для сериализации protocol buffer данных. Указатель на структуру protocol buffer сообщения реализует интерфейс proto.Message. Вызов proto.Marshal возвращает protocol buffer, закодированный в его проводном формате. Например, мы используем эту функцию в команде 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(b []byte, m Message) из библиотеки proto. Вызов этой функции анализирует данные в b как protocol buffer и помещает результат в m. Итак, чтобы проанализировать файл в команде 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 определение. Если вы хотите, чтобы ваши новые буферы имели обратную совместимость, а старые буферы - прямую совместимость - а вы почти наверняка этого хотите - тогда вам нужно соблюдать некоторые правила. В новой версии protocol buffer:

  • вы не должны изменять номера тегов каких-либо существующих полей.
  • вы можете удалять поля.
  • вы можете добавлять новые поля, но вы должны использовать свежие номера тегов (т.е. номера тегов, которые никогда не использовались в этом protocol buffer, даже в удаленных полях).

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

Однако имейте в виду, что новые поля не будут присутствовать в старых сообщениях, поэтому вам нужно будет сделать что-то разумное со значением по умолчанию. Используется значение по умолчанию, зависящее от типа: для строк значением по умолчанию является пустая строка. Для логических значений значение по умолчанию - false. Для числовых типов значение по умолчанию равно нулю.


Читайте также:


Комментариев нет:

Отправить комментарий