Здравствуйте и добро пожаловать на gRPC курсе. На предыдущих лекциях мы узнали как создать protobuf сообщения и сгенерировать Go и Java код из них. Сегодня мы начнём использовать эти сгенерированные коды для сериализации объекта в двоичный код и JSON. На первой половине лекции мы будем писать код на Go. А именно мы запишем protobuf сообщение в двоичный файл. Затем считаем содержимое этого файла и сохраним в другом сообщении. Мы также запишем сообщение в JSON файл, а затем сравним его размер с двоичным файлом, чтобы посмотреть какой из них меньше. На второй половине лекции мы реализуем то же самое, но будем писать код на Java. Запишем protobuf сообщение в двоичный файл, считаем его, затем запишем в JSON файл. Но в этот раз мы также попробуем прочитать двоичный файл, который был создан программой на Go и запишем его в другой JSON файл, чтобы сравнить с файлом, сгенерированным программой на Java. Итак, приступим!
Я открою pcbook
проект на Go, над которым мы работали на предыдущих лекциях.
Прежде всего нам нужно выполнить команду go mod init
для инициализации нашего
пакета. Я назову его gitlab.com/techschool/pcbook
.
go mod init gitlab.com/techschool/pcbook
После выполнения этой команды в папке с проектом будет создан go.mod
файл.
Теперь давайте создадим пакет sample
, чтобы сгенерировать тестовые данные,
касающихся характеристик ноутбука. Я люблю генерировать случайные данные,
потому что их удобно использовать при написании unit тестов, так как
будут выдаваться различные значения при каждом вызове и данные выглядят
естественно и близки к реальным.
Итак, для начала нам нужна клавиатура, поэтому я определю функцию
NewKeyboard
, которая возвращает указатель на объект pb.Keybord
. Хорошо, что
Visual Studio Code автоматически импортирует для нас правильный пакет. Visual
Studio Code выдаёт предупреждение, поскольку все экспортируемые функции
(начинающиеся с большой буквы) в Go должны иметь комментарии. Задайте
комментарий и определите клавиатуру следующим образом:
sample/generator.go
package sample
// NewKeyboard returns a new sample keyboard
func NewKeyboard() *pb.Keyboard {
keyboard := &pb.Keyboard{
Layout: randomKeyboardLayout(),
Backlit: randomBool(),
}
return keyboard
}
Клавиатура имеет раскладку, поэтому я напишу функцию для генерации случайной
раскладки клавиатуры, а также функцию, генерирующую случайное логическое
значение для поля, отвечающего за подсветку. Давайте создадим новый файл
random.go
. Она находится в том же пакете sample
. Я создам в нём две функции
только объявив их, пока без содержимого.
sample/random.go
package sample
func randomBool() bool {
}
func randomKeyboardLayout() pb.Keyboard_Layout {
}
Теперь давайте реализуем сначала функцию randomBool
, поскольку она проще.
Логической переменной можно присвоить только одно из двух значений: true
или
false
, поэтому я буду использовать функцию rand.Intn
из пакета math/rand
с n
равным 2. Она возвращает случайное целое число: 0 или 1. Таким образом,
будем возвращать true
, если значение равно 1, и false
- в противном случае.
Функция randomKeyboardLayout
возвращает одно из трех возможных значений,
поэтому будем использовать rand.Intn(3)
. Если значение равно 1, то вернем
QWERTY
, если 2 - QWERTZ
, иначе - AZERTY
.
sample/random.go
package sample
func randomBool() bool {
return rand.Intn(2) == 1
}
func randomKeyboardLayout() pb.Keyboard_Layout {
switch rand.Intn(3) {
case 1:
return pb.Keyboard_QWERTY
case 2:
return pb.Keyboard_QWERTZ
default:
return pb.Keyboard_AZERTY
}
}
Затем создадим функцию для генерации ЦПУ со случайными параметрами. Сначала я создам пустой объект ЦПУ и верну его.
sample/generator.go
func NewCPU() *pb.CPU {
cpu := &pb.CPU{
}
return cpu
}
Если навести на название структуры, то увидим множество полей, которые
необходимо заполнить. Нам нужна функция, которая вернет случайную строку с
фирмой производителем ЦПУ. Давайте перейдём в файл random.go
и определим её.
Один из простейших способов сделать это — выбрать случайное значение из
заранее определенного множества фирм производителей, например, "Intel" и
"AMD".
sample/random.go
func randomCPUBrand() string {
return randomStringFromSet("Intel", "AMD")
}
func randomStringFromSet(a ...string) string {
n := len(a)
if n == 0 {
return ""
}
return a[rand.Intn(n)]
}
Здесь я определил функцию randomStringFromSet
, которая принимает на вход
произвольное число строк и возвращает одну случайную строку из них. Её очень
просто написать, используйте функцию rand.Intn()
, как мы делали до этого.
Теперь мы можем заполнить поле с фирмой производителем для ЦПУ. Затем мы
сгенерируем случайное название ЦПУ, используя название фирмы производителя в
этой функции. Поскольку существует только две фирмы производителя, то простого
if
будет достаточно. Чтобы не вводить все возможные названия, я ограничился
некоторым заранее определенным множеством. Теперь мы можем задать значение для
поля с названием ЦПУ.
sample/random.go
func randomCPUName(brand string) string {
if brand == "Intel" {
return randomStringFromSet(
"Xeon E-2286M",
"Core i9-9980HK",
"Core i7-9750H",
"Core i5-9400F",
"Core i3-1005G1",
)
}
return randomStringFromSet(
"Ryzen 7 PRO 2700U",
"Ryzen 5 PRO 3500U",
"Ryzen 3 PRO 3200GE",
)
}
Следующее поле, которое мы должны заполнить — это число ядер. Допустим, что
это число должно быть в диапазоне от 2 до 8. Таким образом, нам понадобится
функция randomInt()
, для создания случайного целого числа между min
и
max
.
sample/random.go
func randomInt(min, max int) int {
return min + rand.Intn(max-min+1)
}
Здесь используется формула min + rand.Intn(max-min+1)
. В этой формуле функция
rand.Intn
возвращает целое число в диапазоне от 0
до max-min
. Поэтому
если мы прибавим min
к этому числу, то получим значение от min
до max
.
Ничего сложного, правда?
Итак, теперь мы можем задать значение для поля numberCores
. Оно ожидает
значение типа unsigned int32
, поэтому здесь нам необходимо осуществить
преобразование типа. По аналогии для числа потоков мы будем использовать
случайное целое число в диапазоне от числа ядер до 12. Следующее поле -
minGhz
с типом float64
. Я хочу, чтобы процессор имел минимальную частоту в
диапазоне от 2.0 до 3.5, поэтому нам нужна функция, создающая float64
в
промежутке от min
до max
.
sample/random.go
func randomFloat64(min, max float64) float64 {
return min + rand.Float64()*(max-min)
}
Она немного отличается от функции randomInt
. Поскольку функция
rand.Float64()
возвращает случайное число с плавающей запятой в диапазоне от
0
до 1
, мы должны умножить его на (max-min)
, чтобы получить значение в
промежутке от 0
до max-min
. Когда мы прибавляем min
к этому значению, мы
получаем число в диапазоне от min
до max
. Надеюсь вы поняли. Вернемся к
нашему генератору. Для максимальной частоты будем использовать случайное
значение типа float64
в диапазоне от min
частоты до 5.0
ГГц. Теперь мы
можем задать значения для полей minGhz
и maxGhz
ЦПУ.
sample/generator.go
func NewCPU() *pb.CPU {
brand := randomCPUBrand()
name := randomCPUName(brand)
numberCores := randomInt(2, 8)
numberThreads := randomInt(numberCores, 12)
minGhz := randomFloat64(2.0, 3.5)
maxGhz := randomFloat64(minGhz, 5.0)
cpu := &pb.CPU{
Brand: brand,
Name: name,
NumberCores: uint32(numberCores),
NumberThreads: uint32(numberThreads),
MinGhz: minGhz,
MaxGhz: maxGhz,
}
return cpu
}
Подобным образом можно реализовать функцию NewGPU
. Мы создадим функцию,
возвращающую случайную строку с фирмой производителем GPU, которая может быть
равна NVIDIA или AMD. Затем мы сгенерируем случайное название GPU, используя
название фирмы производителя.
sample/random.go
func randomGPUBrand() string {
return randomStringFromSet("NVIDIA", "AMD")
}
func randomGPUName(brand string) string {
if brand == "NVIDIA" {
return randomStringFromSet(
"RTX 2060",
"RTX 2070",
"GTX 1660-Ti",
"GTX 1070",
)
}
return randomStringFromSet(
"RX 590",
"RX 580",
"RX 5700-XT",
"RX Vega-56",
)
}
Как и для ЦПУ, чтобы не вводить все возможные названия, я ограничился
некоторым заранее определенным множеством. Значения для полей minGhz
и
maxGhz
генерируются, используя функцию randomFloat64
, которую мы определили
ранее. Пусть минимальная частота может меняться в диапазоне от 1.0
до 1.5
,
а максимальная меняется от этого min
до 2.0
ГГц. Осталось заполнить одно
поле: память. Для этого нам нужно создать новый объект Memory
. Допустим мы
хотим, чтобы она была в диапазоне от 2 до 6 гигабайт. Таким образом, нам нужно
использовать функцию randomInt
с преобразованием типа в unsigned int64
.
В качестве единиц измерения используйте значение Memory_GIGABYTE
из
перечисления, сгенерированного protoc
за нас. Мы оценим удобство от
использования этого перечисления в дальнейшем. На этом мы завершим работу с
функцией для GPU.
sample/generator.go
func NewGPU() *pb.GPU {
brand := randomGPUBrand()
name := randomGPUName(brand)
minGhz := randomFloat64(1.0, 1.5)
maxGhz := randomFloat64(minGhz, 2.0)
memory := &pb.Memory{
Value: uint64(randomInt(2, 6)),
Unit: pb.Memory_GIGABYTE,
}
gpu := &pb.GPU{
Brand: brand,
Name: name,
MinGhz: minGhz,
MaxGhz: maxGhz,
Memory: memory,
}
return gpu
}
Определить функцию для генерации ОЗУ очень просто, поскольку она практически ничем не отличается от памяти GPU.
sample/generator.go
func NewRAM() *pb.Memory {
ram := &pb.Memory{
Value: uint64(randomInt(4, 64)),
Unit: pb.Memory_GIGABYTE,
}
return ram
}
Теперь займемся накопителями. Мы создадим две функции: одну для SSD, а другую
для HDD. Для SSD в качестве драйвера будем использовать значение Storage_SSD
,
а объём накопителя будет в пределах от 128
до 1024
гигабайт. Я скопирую
функцию NewSSD()
и изменю название на HDD. Здесь в качестве драйвера мы
должны использовать Storage_HDD
, а объём зададим в диапазоне от 1 до 6
терабайт. Отлично!
sample/generator.go
func NewSSD() *pb.Storage {
ssd := &pb.Storage{
Driver: pb.Storage_SSD,
Memory: &pb.Memory{
Value: uint64(randomInt(128, 1024)),
Unit: pb.Memory_GIGABYTE,
},
}
return ssd
}
func NewHDD() *pb.Storage {
hdd := &pb.Storage{
Driver: pb.Storage_HDD,
Memory: &pb.Memory{
Value: uint64(randomInt(1, 6)),
Unit: pb.Memory_TERABYTE,
},
}
return hdd
}
Теперь займемся функцией для экрана. Размер экрана может меняться в диапазоне
от 13 до 17 дюймов. Это значение имеет тип float32
, поэтому определим
функцию randomFloat32
. Она будет отличаться от randomFloat64
только тем,
что использует значения с типом float32
.
sample/random.go
func randomFloat32(min float32, max float32) float32 {
return min + rand.Float32()*(max-min)
}
Займемся разрешением экрана. Для высоты будем использовать случайное целое число в диапазоне от 1080 до 4320, а ширину вычислять по высоте, используя соотношение сторон 16 на 9.
sample/random.go
func randomScreenResolution() *pb.Screen_Resolution {
height := randomInt(1080, 4320)
width := height * 16 / 9
resolution := &pb.Screen_Resolution{
Height: uint32(height),
Width: uint32(width),
}
return resolution
}
Здесь мы должны выполнить преобразование типа, поскольку ширина и высота должны
иметь тип unsigned int32
. Теперь зададим типа матрицы экрана. Напишем
отдельную функцию, возвращающую случайную строку. В нашем приложении только два
типа матриц: IPS
или OLED
. Таким образом, достаточно использовать функцию
rand.Intn(2)
и оператор if
.
sample/random.go
func randomScreenPanel() pb.Screen_Panel {
if rand.Intn(2) == 1 {
return pb.Screen_IPS
}
return pb.Screen_OLED
}
Последнее поле, которое мы должны задать — мультитач — это просто случайное логическое значение.
sample/generator.go
func NewScreen() *pb.Screen {
screen := &pb.Screen{
SizeInch: randomFloat32(13, 17),
Resolution: randomScreenResolution(),
Panel: randomScreenPanel(),
Multitouch: randomBool(),
}
return screen
}
Итак, все компоненты созданы. Теперь мы можем сгенерировать новый ноутбук.
Ему нужен уникальный случайный идентификатор. Для этого давайте создадим
функцию randomID()
. Я буду использовать пакет Google UUID. Мы можем найти его
через поисковик браузера, скопировать команду go get
и запустить её в
терминале, чтобы установить пакет.
go get github.com/google/uuid
Теперь вернемся к нашему коду. Мы можем вызвать функцию uuid.New()
, чтобы
получить случайный идентификатор и преобразовать его в строку.
sample/random.go
func randomID() string {
return uuid.New().String()
}
Затем мы сгенерируем фирму производителя ноутбука и название по аналогии с ЦПУ и GPU. Я опять буду использовать значения из предопределенного множества, чтобы не перечислять все возможные значения.
sample/random.go
func randomLaptopBrand() string {
return randomStringFromSet("Apple", "Dell", "Lenovo")
}
func randomLaptopName(brand string) string {
switch brand {
case "Apple":
return randomStringFromSet("Macbook Air", "Macbook Pro")
case "Dell":
return randomStringFromSet("Lalitude", "Vostro", "XPS", "Alienware")
default:
return randomStringFromSet("Thinkpad X1", "Thinkpad P1", "Thinkpad P53")
}
}
Будем создавать ноутбуки фирм Apple
, Dell
и Lenovo
. Мы используем
оператор switch-case
, чтобы сгенерировать правильное название ноутбука в
зависимости от фирмы производителя. Затем зададим ЦПУ и ОЗУ, вызвав
соответствующие функции-генераторы. Поле для GPU является списком, поэтому в
качестве значения я определю список. Пусть у всех ноутбуков пока будет по одну
GPU. Аналогично создадим список для накопителей, но в этот раз я добавлю в него
два элемента: один для SSD, а другой для HDD. Поля для экрана и клавиатуры
заполнить несложно — просто используйте генераторы. Далее следует поле
oneof
: Weight
(Вес). Мы можем указать вес в килограммах или фунтах.
Protoc
создал для нас две структуры. Я собираюсь использовать здесь
килограммы. В структуре pb.Laptop_WeightKg
есть поле WeightKg
. Зададим
для него значение в диапазоне от одного до трёх килограмм. Цена — это случайное
число в диапазоне от 1500 до 3000. Год выпуска зададим от 2015 до 2019. И,
наконец, для временной метки updateAt
мы можем использовать функцию Now()
,
предоставляемую пакетом google.golang.org/protobuf/types/known/timestamppb
.
Мы закончили!
sample/generator.go
func NewLaptop() *pb.Laptop {
brand := randomLaptopBrand()
name := randomLaptopName(brand)
laptop := &pb.Laptop{
Id: randomID(),
Brand: brand,
Name: name,
Cpu: NewCPU(),
Ram: NewRAM(),
Gpus: []*pb.GPU{NewGPU()},
Storages: []*pb.Storage{NewSSD(), NewHDD()},
Screen: NewScreen(),
Keyboard: NewKeyboard(),
Weight: &pb.Laptop_WeightKg{
WeightKg: randomFloat64(1.0, 3.0),
},
PriceUsd: randomFloat64(1500, 3000),
ReleaseYear: uint32(randomInt(2015, 2019)),
UpdatedAt: timestamppb.Now(),
}
return laptop
}
Теперь создадим новый пакет serializer
и реализуем в нём несколько функций
для сериализации объектов-ноутбуков в файлы. Итак, давайте создадим файл
file.go
в папке serializer
.
Первая функция будет записывать protobuf сообщение в файл в двоичном формате.
В нашем случае сообщением будет объект-ноутбук. Мы можем использовать интерфейс
proto.Message
, чтобы функция могла принимать любые сообщения в качестве
входного аргумента. В этой функции нам нужно сначала вызвать proto.Marshal
,
чтобы сериализовать сообщение в двоичную форму. Если возникает ошибка мы просто
обертываем её и возвращаем вызывающему. В противном случае мы используем
функцию ioutil.WriteFile()
для сохранения данных в файле с указанным именем.
Опять обертываем и возвращаем любую ошибку, возникающую во время этого
процесса. Если всё прошло удачно, мы просто возвращаем nil
, что означает
отсутствие ошибок. Функция готова.
package serializer
import (
"fmt"
"io/ioutil"
"github.com/golang/protobuf/proto"
)
// WriteProtobufToBinaryFile writes protocol buffer message to binary file
func WriteProtobufToBinaryFile(message proto.Message, filename string) error {
data, err := proto.Marshal(message)
if err != nil {
return fmt.Errorf(
"cannot marshal proto message to binary: %w",
err,
)
}
err = ioutil.WriteFile(filename, data, 0644)
if err != nil {
return fmt.Errorf("cannot write binary data to file: %w", err)
}
return nil
}
Я покажу как написать unit текст для неё. Давайте создадим файл
file_test.go
в папке serializer
. Обратите внимание, что суффикс _test
в названии файла является обязательным, иначе Go не сможет понять, что это
тестовый файл. Кроме того принято определенным образом называть функции
unit теста. Они должны начинаться с префикса Test и принимать в качестве
входных данных указатель на объект testing.T
. Я обычно вызываю t.Parallel()
для почти всех моих unit тестов, чтобы они запускались параллельно и можно
было легко обнаружить любое состояние гонки.
func TestFileSerializer(t *testing.T) {
t.Parallel()
}
Допустим мы хотим сериализовать объект в файл laptop.bin
внутри папки tmp
.
Итак, сначала нам надо создать папку tmp
. Затем, используя функцию
NewLaptop()
сгенерировать новый laptop1
и вызвать функцию
WriteProtobufToBinaryFile()
, чтобы сохранить его в файл laptop.bin
.
Поскольку эта функция может возвращать ошибку, мы должны убедиться, что ошибка
равна nil
. Это означает, что файл успешно записан. Для подобных проверок, я
часто использую пакет testify
. Откройте его страницу на github, чтобы
скопировать go get
команду и запустить её в вашем терминале.
go get github.com/stretchr/testify
Теперь мы можем просто вызвать функцию require.NoError()
с параметрами t
и err
.
package serializer
import (
"testing"
"github.com/MaksimDzhangirov/complete-gRPC/code/lecture9.1/sample"
"github.com/MaksimDzhangirov/complete-gRPC/code/lecture9.1/serializer"
"github.com/stretchr/testify/require"
)
func TestFileSerializer(t *testing.T) {
t.Parallel()
binaryFile := "../tmp/laptop.bin"
laptop1 := sample.NewLaptop()
err := serializer.WriteProtobufToBinaryFile(laptop1, binaryFile)
require.NoError(t, err)
}
В Visual Studio Code мы можем нажать на ссылку "Run test", расположенной на
строчку выше самого теста, чтобы запустить его. После запуска произошла ошибка:
import cycle not allowed in test
. Она связана с тем, что мы находимся в
пакете serializer
и пытаемся импортировать его. Просто добавьте _test
к
имени нашего пакета, чтобы изменить его название, а также указать Go, что это
тестовый пакет. Теперь если мы повторно запустим тест, то он будет успешно
пройден. Отлично! Как мы видим, файл laptop.bin
записался в папку tmp
.
Теперь мы напишем другую функцию для считывания этого бинарного файла в объект
protobuf сообщения. Я назову эту функцию ReadProtobufFromBinaryFile()
.
Сначала нам нужно использовать ioutil.ReadFile()
для чтения двоичных данных
из файла. Затем мы вызываем proto.Unmarchal()
, чтобы десериализовать двоичные
данные в protobuf сообщение.
serializer/file.go
// ...
func ReadProtobufFromBinaryFile(filename string, message proto.Message) error {
data, err := ioutil.ReadFile(filename)
if err != nil {
return fmt.Errorf("cannot read binary data from file: %w", err)
}
err = proto.Unmarshal(data, message)
if err != nil {
return fmt.Errorf(
"canot unmarshal binary to proto message: %w",
err,
)
}
return nil
}
Давайте протестируем его. В нашем unit тесте я определю новый объект
laptop2
и вызову ReadProtobufFromBinaryFile()
, чтобы считать данные файла
в этот объект. Мы проверим нет ли ошибок и мы также хотим убедиться, что
laptop2
содержит те же данные, что и laptop1
. Для этого мы можем
использовать функцию proto.Equal
из пакета golang/protobuf
. Эта функция
должна возвращать true
, поэтому здесь мы осуществляем проверку
require.True()
. Теперь запустим тест. Он был успешно пройден!
// ...
func TestFileSerializer(t *testing.T) {
// ...
laptop2 := &pb.Laptop{}
err = serializer.ReadProtobufFromBinaryFile(binaryFile, laptop2)
require.NoError(t, err)
require.True(t, proto.Equal(laptop1, laptop2))
}
Теперь поскольку данные записаны в двоичном формате, мы не можем просмотреть
их. Давайте напишем другую функцию для их сериализации в JSON
формат. В этой
функции мы должны сначала преобразовать protobuf сообщение в JSON
строку.
Для этого я создам новую функцию с именем ProtobufToJSon
. Её код поместите в
отдельный файл json.go
в тот же пакет serializer
. Итак, теперь, чтобы
преобразовать protobuf сообщение в JSON
, мы можем использовать структуру
jsonb.Marshaler
. По сути, нам нужно просто вызвать функцию
marshaler.MarshalToString()
.
serializer/json.go
package serializer
import (
"github.com/golang/protobuf/jsonpb"
"github.com/golang/protobuf/proto"
)
func ProtobufToJSON(message proto.Message) (string, error) {
marshaler := jsonpb.Marshaler{
EnumsAsInts: false,
EmitDefaults: true,
Indent: " ",
OrigName: true,
}
return marshaler.MarshalToString(message)
}
Здесь мы можем настроить несколько параметров, например, записывать
перечисления в виде целых чисел или строк (EnumsAsInts
), записывать поля со
значениями по умолчанию или нет (EmitDefaults
), какой отступ мы хотим
использовать (Indent
), хотим ли мы использовать исходные названия полей,
определенные в proto файле (OrigName
). Давайте пока будем использовать
настройки, приведенные выше, а другие значения для них зададим позже. Теперь
вернемся к нашей функции. После вызова ProtobufToJSON
, мы получили JSON
строку. Всё, что нам осталось сделать, это записать эту строку в файл.
serializer/file.go
func WriteProtobufToJSONFile(message proto.Message, filename string) error {
data, err := ProtobufToJSON(message)
if err != nil {
return fmt.Errorf(
"cannot marshal proto message to JSON: %w",
err,
)
}
err = ioutil.WriteFile(filename, []byte(data), 0644)
if err != nil {
return fmt.Errorf("cannot write JSON data to file: %w", err)
}
return nil
}
Теперь давайте вызовем эту функцию в нашем модельном тесте.
// ...
func TestFileSerializer(t *testing.T) {
// ...
err = serializer.WriteProtobufToJSONFile(laptop1, jsonFile)
require.NoError(t, err)
}
Добавим проверку на отсутствие ошибок после вызова этой функции и запустим
тест. Вуаля, был успешно создан файл laptop.json
. Как видите, названия полей
точно такие же как мы определили в наших proto файлах, то есть маленькими
буквами в snake case стиле.
Теперь, если мы изменим параметр OrigName
на false
, и повторно запустим
тест, то названия полей изменятся на сamel сase стиль, где все слова начинаются
с маленькой буквы. Сейчас все значения, задаваемые в полях-перечислениях,
записываются в виде строк, например, IPS
для типа матрицы экрана. Если мы
изменим значение параметра EnumsAsInts
на true
и повторно запустим тест,
то значение типа матрицы изменится на целое число 1.
Теперь я хочу убедиться, что сгенерированные ноутбуки будут иметь различные
характеристики каждый раз при запуске тестов. Давайте выполним команду go test ./...
несколько раз и посмотрим что произойдёт.
go test ./...
...
означает, что мы хотим запустить unit тесты для всех пакетов,
находящихся в подкаталогах текущего каталога. Похоже, что в JSON файле
меняется только уникальный идентификатор, а остальные значения не меняются.
Это происходит из-за того, что по умолчанию пакет rand
использует
фиксированное начальное значение. Мы можем указать ему использовать различные
значения при каждом запуске. Для этого давайте создадим init()
функцию в
файле random.go
. Это специальная функция, которая будет вызываться единожды
перед выполнением любого другого кода в пакете. В этой функции мы укажем
rand
, что нужно использовать текущее значение времени в наносекундах в
качестве начального значения.
sample/random.go
func init() {
rand.Seed(time.Now().UnixNano())
}
Теперь давайте запустим тест несколько раз. Мы видим, что характеристики
ноутбука меняются. Превосходно!
Мы также можем выполнить команду go test ./serializer/file_test.go
, чтобы
прогнать тесты только из этого файла. Теперь давайте зададим команду
make test
в make-файле для запуска unit тестов. Мы можем использовать
флаг -cover
, чтобы измерить покрытие кода нашими тестами и флаг -race
для
обнаружения любых состояний гонки в нашем коде.
# ...
test:
go test -cover -race ./...
# ...
Теперь запустите команду make test
в терминале.
make test
Как видите, в пакете serializer
тестами покрыто 73.9% кода. Мы можем также
перейти в файл с тестами и нажать на "run package tests" в самом верху. Это
запустит все тесты в пакете и сообщит о покрытии кода. Затем мы можем открыть
файлы и просмотреть какая часть нашего кода покрыта тестами, а какая — нет.
Покрытый код выделен синим, а не покрытый — красным. Всегда нужно стараться
писать такой набор тестов, который будет покрывать различные ветки выполнения
кода.
Я хочу показать ещё кое-что прежде, чем мы перейдём к проекту на Java. Давайте
откроем терминал, зайдём в папку tmp
и выполним команду ls -l
. Мы увидим,
что размер json-файла примерно в 5 раз больше, чем двоичного. Таким образом,
мы сэкономим много трафика, если будем использовать gRPC вместо обычного JSON
API. Кроме того, так как размер сообщения меньше, то его можно быстрее
передать. Это большое преимущество двоичного протокола. Увидимся в разделе,
который посвящен проекту на Java!