Добро пожаловать на вторую лекцию о 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
(единица измерения). Неплохо!
Вернемся к файлу 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 файлу
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"
]
}
}
Тем не менее, если я добавлю лишнюю табуляцию и сохраню файл
processor_message.proto
автоматического форматирования кода не произойдет при
сохранении. Хотя на последней лекции мы установили расширение для вызова
библиотеки clang-format
, мы не установили саму библиотеку. Поэтому давайте
установим её с помощью Homebrew и перезапустим Visual Studio Code. Теперь я
опять добавлю табуляцию и сохраню файл. Всё работает! Отлично! Clang-format
автоматически отформатировал файлы, используя отступы в два пробела.
Продолжим работать с нашим проектом. Я хочу создать новое сообщение для
накопителей. Накопителем может быть устройство на жестких магнитных дисках или
твердотельное устройство. Поэтому мы создадим перечисление 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
. Код успешно сгенерируется.
Имя пакета, которое protoc
генерирует автоматически, слишком длинное и не
совпадает с названием папки pb
. Я хочу указать компилятору, что нужно
использовать pb
в качестве имени пакета, но только для Go, поскольку Java
или другие языки будут использовать другое соглашение о наименовании пакетов.
Для этого мы можем использовать настройку option go_package=".;pb"
в наших
proto файлах. Мы можем запускать команды не только из отдельного окна
терминала, но и из вкладки Terminal
интегрированной среды разработки. Теперь,
если мы запустим команду make gen
, во всех сгенерированных Go файлах pb
будет использоваться как имя пакета.
Далее мы определим сообщение 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
). Он
содержит тип "вложенное сообщение": разрешение (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
. Оно состоит из уникального идентификатора
типа string
, а также фирмы изготовителя, названия, затем ЦПУ и ОЗУ. Нам нужно
импортировать другие proto файлы, чтобы мы могли использовать эти типы.
В ноутбуке может быть несколько GPU, поэтому нам нужно использовать ключевое
слово "repeated", чтобы указать protoc
, что это поле является списком.
Аналогично ноутбук может иметь несколько накопителей, поэтому это поле также
имеет тип repeated
. Затем идут два обычных поля: экран и клавиатура. Всё
довольно просто.
Пусть вес ноутбука может быть задан в килограммах или фунтах. Как это правильно
описать? Для этого мы можем использовать новое ключевое слово: 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 файлов. Спасибо за потраченное время и до новых встреч!