Skip to content

Latest commit

 

History

History
402 lines (337 loc) · 19.7 KB

lecture7_rus.md

File metadata and controls

402 lines (337 loc) · 19.7 KB

Более глубокое изучение Protobuf и настройки Go package

Добро пожаловать на вторую лекцию о protocol buffer. На последней лекции мы изучили базовый синтаксис и типы данных. Теперь давайте более подробно рассмотрим эти темы. Вот что мы собираемся делать:

  • мы определим и будем использовать пользовательские типы в полях сообщения protocol buffer, например, перечисления (enums) или другие сообщения.
  • мы обсудим когда стоит использовать вложенные типы, а когда нет.
  • мы также узнаем о некоторых часто используемых типах, которые уже были определены Google.
  • мы создадим несколько сообщений, каждое в отдельном файле, поместим их в пакет и затем импортируем туда, куда необходимо.
  • мы познакомимся с полями типа repeated и one of, а также узнаем как использовать определенную настройку, чтобы указать protoc, что нужно сгенерировать Go код с заданным именем пакета.

Несколько сообщений в одном файле

Итак, давайте начнём с того места, где мы остановились в последней лекции. В одном proto файле мы можем определить несколько сообщений, поэтому я добавлю в processor_message.proto GPU сообщение. Это логично, поскольку GPU также является процессором. У него будут поля подобные тем, которые мы определили для ЦПУ, например, фирма-производитель, название, минимальная и максимальная частота. Единственное отличие заключается в том, что у GPU есть своя память.

Пользовательские типы: сообщение и перечисление

Память — часто используемое понятие, которое можно использовать при определении других комплектующих, например, ОЗУ или постоянных накопителей данных (SSD и HDD).

processor_message.proto

syntax = "proto3";

message CPU {
  // Brand of the CPU
  string brand = 1;
  /*
   * Name of the CPU
   */
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  // Memory?
}

Кроме того, она имеет множество различных единиц измерения, таких как килобайт, мегабайт, гигабайт или терабайт. Поэтому я определю её как пользовательский тип в отдельном proto файле, чтобы мы могли его повторно использовать позднее. Давайте назовём его memory_message.proto.

memory_message.proto

syntax = "proto3";

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }
  
  uint64 value = 1;
  Unit unit = 2;
}

Во-первых, нам нужно определить единицы измерения. Для этого мы будем использовать тип enum. И поскольку эта единица измерения имеет смысл только в контексте памяти, мы должны определить её как вложенный тип внутри сообщения Memory. Для перечислений всегда принято использовать специальное значение, которое будет значением по умолчанию, и ему следует присвоить тег 0. Затем мы можем добавить другие единицы измерения, от BIT до TERABYTE. Я знаю, что терабайтами всё не ограничивается, но для этого приложения их будет достаточно. Итак, мы определили сообщение Memory с полями value (значение) и unit (единица измерения). Неплохо!

Импортируем proto файлы

Вернемся к файлу processor_message.proto. Мы должны импортировать файл memory_message.proto, чтобы можно было использовать тип Memory. В GPU сообщение мы добавим новое поле типа Memory.

processor_message.proto

syntax = "proto3";

import "memory_message.proto";

message CPU {
  // Brand of the CPU
  string brand = 1;
  /*
   * Name of the CPU
   */
  string name = 2;
  uint32 number_cores = 3;
  uint32 number_threads = 4;
  double min_ghz = 5;
  double max_ghz = 6;
}

message GPU {
  string brand = 1;
  string name = 2;
  double min_ghz = 3;
  double max_ghz = 4;
  Memory memory = 5;
}

Давайте попробуем повторно сгенерировать Go код.

make gen

Если мы не добавим опцию package в файлы, то получим ошибку

Go package "." has inconsistent names memory_message (memory_message.proto)
and processor_message (processor_message.proto)

Мы получили ошибку, так как не указали название пакета в proto файлах, а по умолчанию protoc будет использовать имя файла для имени Go пакета. Причина, из-за которой protoc выдает ошибку, заключается в том, что два сгенерированных Go файла относятся к двум различным пакетам. А в Go, мы не можем добавить в одну папку файлы из разных пакетов. В нашем случае, папку pb.

Поэтому мы должны указать protoc, что их следует поместить в один пакет, явно определив его название в proto файлах. Давайте назовём его techschool_pcbook. Теперь повторно выполните команду make gen.

syntax = "proto3";

package techschool_pcbook;

message Memory {
  enum Unit {
    UNKNOWN = 0;
    BIT = 1;
    BYTE = 2;
    KILOBYTE = 3;
    MEGABYTE = 4;
    GIGABYTE = 5;
    TERABYTE = 6;
  }

  uint64 value = 1;
  Unit unit = 2;
}
make gen

Всё заработало! Если мы откроем два сгенерированных Go файла, то увидим, что они относятся к одному и тому же пакету techschool_pcbook. It works! If we open the 2 generated Go files, we can see that they have the same package techschool_pcbook.

Обновляем настройку proto_path для VSCode

Теперь я хочу вам показать кое-что. Вернемся к нашему proto файлу processor_message.proto. Несмотря на то, что мы успешно сгенерировали Go код, Visual Studio Code всё ещё подчеркивает красными линиями тип Memory и команду импорта. Проблема в том, что по умолчанию расширение vscode proto3 использует нашу текущую рабочую папку в качестве proto_path, когда запускает protoc для анализа кода. Таким образом, он не может найти файл memory_message.proto в папке lecture6 для импорта. Если мы изменим путь в файле processor_message.proto на proto/memory_message.proto, то ошибка пропадёт. Но я не хочу этого делать, поскольку позднее мы будем использовать эти proto файлы в нашем Java проекте с другой структурой каталогов. Поэтому я покажу вам как исправить эту ошибку, изменив настройки proto_path расширения vscode-proto3. Давайте откроем вкладку с расширениями и найдём vscode-proto3. Мы увидим нужные настройки, но скопировать их не получится. Поэтому я перейду на сайт и скопирую настройки оттуда. Теперь перейдите в меню Code, Preference, Settings и в поисковой строке найдите protoc. Нажмите Edit in settings.json. Вставьте сюда скопированные настройки. Мы можем узнать путь к protoc, выполнив команду:

which protoc

в терминале.

Затем поменяйте путь proto_path на proto. Давайте сохраним этот файл и перезапустим Visual Studio Code. Ошибка пропала.

{
    "protoc": {
        "path": "/usr/local/bin/protoc",
        "options": [
            "--proto_path=proto"
        ]
    }
}

Устанавливаем clang-format для автоматического форматирования кода

Тем не менее, если я добавлю лишнюю табуляцию и сохраню файл processor_message.proto автоматического форматирования кода не произойдет при сохранении. Хотя на последней лекции мы установили расширение для вызова библиотеки clang-format, мы не установили саму библиотеку. Поэтому давайте установим её с помощью Homebrew и перезапустим Visual Studio Code. Теперь я опять добавлю табуляцию и сохраню файл. Всё работает! Отлично! Clang-format автоматически отформатировал файлы, используя отступы в два пробела.

Определяем сообщение Storage

Продолжим работать с нашим проектом. Я хочу создать новое сообщение для накопителей. Накопителем может быть устройство на жестких магнитных дисках или твердотельное устройство. Поэтому мы создадим перечисление Driver с этими двумя значениями. И не забудьте указать значение по умолчанию. Теперь давайте добавим 2 поля к сообщению Storage: тип устройства (driver) и объем (memory).

storage_message.proto

syntax = "proto3";

package techschool_pcbook;

import "memory_message.proto";

message Storage {
  enum Driver {
    UNKNOWN = 0;
    HDD = 1;
    SSD = 2;
  }
  
  Driver driver = 1;
  Memory memory = 2;
}

Затем выполните make gen. Код успешно сгенерируется.

Используем настройку для задания имени Go пакета

Имя пакета, которое protoc генерирует автоматически, слишком длинное и не совпадает с названием папки pb. Я хочу указать компилятору, что нужно использовать pb в качестве имени пакета, но только для Go, поскольку Java или другие языки будут использовать другое соглашение о наименовании пакетов. Для этого мы можем использовать настройку option go_package=".;pb" в наших proto файлах. Мы можем запускать команды не только из отдельного окна терминала, но и из вкладки Terminal интегрированной среды разработки. Теперь, если мы запустим команду make gen, во всех сгенерированных Go файлах pb будет использоваться как имя пакета.

Определяем сообщение Keyboard

Далее мы определим сообщение Keyboard. Для клавиатуры может использоваться QWERTY, QWERTZ или AZERTY раскладка (поле layout). К сведению, QWERTZ широко используется в Германии, тогда как во Франции более популярна AZERTY. Клавиатура может быть с подсветкой или без неё, поэтому давайте будем использовать булево поле backlit для описания этого параметра. Ничего сложного, не так ли?

keyboard_message.proto

syntax = "proto3";

package techschool_pcbook;

option go_package = ".;pb";

message Keyboard {
  enum Layout {
    UNKNOWN = 0;
    QWERTY = 1;
    QWERTZ = 2;
    AZERTY = 3;
  }

  Layout layout = 1;
  bool backlit = 2;
}

Определяем сообщение Screen

Теперь давайте определим более сложное сообщение: экран (Screen). Он содержит тип "вложенное сообщение": разрешение (Resolution). Причина, по которой мы используем здесь вложенный тип, заключается в том, что: разрешение — это сущность, непосредственно связанная с экраном. Оно не имеет смысла само по себе. По аналогии мы создали перечисление для типа матрицы экрана, которое может принимать значения IPS или OLED. Затем мы создали поле size_inch, описывающее размер экрана в дюймах. И, наконец, булево поле multitouch, определяющее это мультитач-экран или нет.

screen_message.proto

syntax = "proto3";

package techschool_pcbook;

option go_package = ".;pb";

message Screen {
  message Resolution {
    uint32 width = 1;
    uint32 height = 2;
  }
  
  enum Panel {
    UNKNOWN = 0;
    IPS = 1;
    OLED = 2;
  }
  
  float size_inch = 1;
  Resolution resolution = 2;
  Panel panel = 3;
  bool multitouch = 4;
}

Определяем сообщение Laptop

Итак, мы определили практически все необходимые компоненты ноутбука. Давайте определим теперь сообщение Laptop. Оно состоит из уникального идентификатора типа string, а также фирмы изготовителя, названия, затем ЦПУ и ОЗУ. Нам нужно импортировать другие proto файлы, чтобы мы могли использовать эти типы.

Поле Repeated

В ноутбуке может быть несколько GPU, поэтому нам нужно использовать ключевое слово "repeated", чтобы указать protoc, что это поле является списком. Аналогично ноутбук может иметь несколько накопителей, поэтому это поле также имеет тип repeated. Затем идут два обычных поля: экран и клавиатура. Всё довольно просто.

Поле Oneof

Пусть вес ноутбука может быть задан в килограммах или фунтах. Как это правильно описать? Для этого мы можем использовать новое ключевое слово: oneof. Здесь мы зададим два поля, одно для килограммов, а другое — для фунтов. Помните, что когда вы используете поле типа oneof, сохранится только последнее значение, которое вы присвоили этому полю.

Часто используемые типы

Теперь добавим ещё два поля: цена в USD и год выпуска ноутбука. И, наконец, нам нужна временная метка для хранения времени последнего обновления записи в нашей системе. Временная метка — это один из часто используемых типов, который уже определен Google. Таким образом, нам просто нужно импортировать пакет и использовать его. Существуют также другие часто используемых типы. Чтобы получить больше информации о них, перейдите по ссылке. Теперь давайте выполним команду make gen, чтобы убедиться, что всё работает без ошибок. Да! Все файлы успешно сгенерированы.

laptop_message.proto

syntax = "proto3";

package techschool_pcbook;

option go_package = ".;pb";

import "processor_message.proto";
import "memory_message.proto";
import "storage_message.proto";
import "screen_message.proto";
import "keyboard_message.proto";
import "google/protobuf/timestamp.proto";

message Laptop {
  string id = 1;
  string brand = 2;
  string name = 3;
  CPU cpu = 4;
  Memory ram = 5;
  repeated GPU gpus = 6;
  repeated Storage storages = 7;
  Screen screen = 8;
  Keyboard keyboard = 9;
  oneof weight {
    double weight_kg = 10;
    double weight_lb = 11;
  }
  double price_usd = 12;
  uint32 release_year = 13;
  google.protobuf.Timestamp updated_at = 14;
}

Ура! Мы многое узнали о protocol buffer и о том, как сгенерировать Golang код с помощью него. На следующей практической лекции мы будем писать код на языке Java. Я покажу вам как настроить Gradle проект, чтобы автоматически генерировать Java код из наших proto файлов. Спасибо за потраченное время и до новых встреч!