Сериализация данных в Golang с Protobuf

Kate

Administrator
Команда форума
Protobuf, или Protocol Buffers, это бинарный формат сериализации, разработанный в Google для эффективного обмена данными между сервисами. Это как JSON, только компактнее, быстрее и типизированнее. Если JSON был вашим первым крашем в мире сериализации, то Protobuf – это тот, с кем вы хотите серьёзных отношений.

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

Установка​

Шаг 1: установка Protobuf Compiler​

Первым делом, вам нужно скачать Protobuf Compiler. Загляните на GitHub страницу Protobuf и выберите версию, подходящую для вашей ОС.

После скачивания распакуйте содержимое и добавьте путь к исполняемому файлу protoc в вашу системную переменную PATH. В Unix это что-то вроде export PATH=$PATH:/path/to/protoc.

Шаг 2: установка Protobuf Plugin​

Запустите go get -u google.golang.org/protobuf/cmd/protoc-gen-go для установки плагина protoc для Go. Этот плагин нужен, чтобы превращать ваши .proto файлы в go-файлы.

Введите protoc --version в консоли. Если вы видите версию, вы успешно установили компилятор!

Cтруктура .proto файлов​

Прежде всего, .proto файл - это схема данных для Protobuf. Это место, где вы описываете структуру данных, которую хотите сериализовать/десериализовать.

Все начинается с указания версии синтаксиса. Например, syntax = "proto3";. Это говорит компилятору, какие правила использовать при анализе содержимого. Затем идет объявление пакета package mypackage;. Это помогает избежать конфликтов имен и организовать код.

Сердце .proto файла - это объявления сообщений. Каждое сообщение - это структура данных, которую вы хотите сериализовать.

Сообщения определяются с использованием синтаксиса message MyMessage {}. Внутри фигурных скобок вы описываете поля данных. Поля могут быть стандартными типами данных, такими как int32, float, double, string или bool. Также могут быть пользовательские типы или другие сообщения.

В Protobuf есть три типа правил для полей: singular для одиночных значений, repeated для массивов значений и map для ассоциативных массивов. Каждому полю присваивается уникальный номер. Эти номера используются в бинарном представлении и очень важны для совместимости данных

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

Предположим, вы хотите сериализовать информацию о техническом обзоре, который включает в себя разделы, заголовки и содержание:

syntax = "proto"

package techreview;

// Объявляем перечисление для различных типов контента
enum ContentType {
UNKNOWN = 0;
INTRODUCTION = 1;
TECHNICAL_OVERVIEW = 2;
BEST_PRACTICES = 3;
CONCLUSION = 4;
}

// Определяем структуру для раздела обзора
message Section {
string title = 1; // Заголовок раздела
string content = 2; // Содержание раздела
ContentType type = 3; // Тип содержимого (введение, обзор и т.д.)
}

// Определяем основную структуру для всего технического обзора
message TechnicalReview {
repeated Section sections = 1; // Массив разделов обзора
}

Enums, Maps​

Создаем Enum в .proto:​

enum Status {
UNKNOWN = 0;
RUNNING = 1;
STOPPED = 2;
}
После компиляции .proto файла, мы получаем четко определенный тип Status, который можно использовать прямо в коде.

var currentStatus Status = Status_RUNNING
Maps позволяет создавать структурированные коллекции пар ключ-значение.

Объявление Map в .proto:​

message Environment {
map<string, string> variables = 1;
}
После компиляции .proto файла, мы получаем map, который можно легко использовать.

env := Environment{
Variables: map[string]string{"HOME": "/home/user", "PATH": "/usr/bin"},
}
Прощай, массивы пар или сложные структуры для простых задач.

Oneof: когда одного поля мало​

oneof - это инструмент в Protobuf для работы с различными структурами в одном поле.

В .proto файле:​

message Command {
oneof command_type {
string text = 1;
int32 number = 2;
}
}
После компиляции .proto, можно управлять этими полями как обычными структурами в Go.

cmd := Command{
CommandType: &Command_Text{Text: "Hello, Protobuf!"},
}

Как автоматически генерировать код из .proto файлов​

Допустим, у нас есть следующий .proto файл, который описывает структуру данных для пользователя:

syntax = "proto ";

package user;

message User {
string name = 1;
int32 age = 2;
string email = 3;
}
Для генерации кода можно юзать команду protoc:

protoc --go_out=. user.proto
После выполнения этой команды в той же директории появится файл user.pb.go, содержащий код на Go для ваших структур данных.

Как можно использовать сгенерированный код на Go:

package main

import (
"fmt"
"log"

"github.com/golang/protobuf/proto"
"path/to/generated/user" // Импорт сгенерированного кода
)

func main() {
newUser := &user.User{
Name: "Alice",
Age: 30,
Email: "alice@example.com",
}

// сериализация данных
data, err := proto.Marshal(newUser)
if err != nil {
log.Fatal("Marshaling error: ", err)
}

// десериализация данных
newUser2 := &user.User{}
err = proto.Unmarshal(data, newUser2)
if err != nil {
log.Fatal("Unmarshaling error: ", err)
}

fmt.Println(newUser2.GetName(), newUser2.GetAge(), newUser2.GetEmail())
}
Для добавления дополнительного функционала можно использовать плагины. Например, protoc-gen-go-grpc для создания gRPC сервера и клиента.

gRPC в protobuf​

Установим нужные пакеты:

go get -u google.golang.org/grpc
go get -u github.com/golang/protobuf/protoc-gen-go
Для начала определим наш gRPC сервис и сообщения в .proto файле:

syntax = "proto3";

package example;

// Определение сервиса
service Greeter {
// Определение метода сервиса
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// Определение сообщения, используемого для запроса
message HelloRequest {
string name = 1;
}

// Определение сообщения, используемого для ответа
message HelloReply {
string message = 1;
}
Следующий шаг - сгенерировать Golang код из нашего .proto файла:

protoc --go_out=plugins=grpc:. *.proto
Реализуем сервер:

package main

import (
"context"
"log"
"net"

"google.golang.org/grpc"
pb "path/to/your/protos/example"
)

// server is used to implement example.GreeterServer.
type server struct {
pb.UnimplementedGreeterServer
}

func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
log.Printf("Received: %v", in.GetName())
return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterGreeterServer(s, &server{})
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
И наконец, реализуем клиент для общения с нашим сервером:

package main

import (
"context"
"log"
"os"
"time"

"google.golang.org/grpc"
pb "path/to/your/protos/example"
)

const (
address = "localhost:50051"
defaultName = "world"
)

func main() {
// Установка соединения с сервером
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewGreeterClient(conn)

// Имя пользователя для приветствия
name := defaultName
if len(os.Args) > 1 {
name = os.Args[1]
}

// Контекст для отмены запроса по истечении таймаута
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

// Вызов метода SayHello на сервере
r, err := c.SayHello(ctx, &pb.HelloRequest{Name: name})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("Greeting: %s", r.GetMessage())
}

Микросервисы​

Предположим, у нас есть микросервис для управления пользователями:

syntax = "proto3";

package user;

// Сервис для работы с пользователями
service UserService {
// Запрос на получение информации о пользователе
rpc GetUser (UserRequest) returns (UserResponse) {}
}

// Запрос на получение информации о пользователе
message UserRequest {
int64 id = 1;
}

// Ответ с информацией о пользователе
message UserResponse {
int64 id = 1;
string name = 2;
string email = 3;
}
Допустим, мы сгенерировали Golang код из нашего файла. Напишем сервер:

package main

import (
"context"
"log"
"net"

"google.golang.org/grpc"
pb "path/to/your/protos/user"
)

type server struct {
pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, in *pb.UserRequest) (*pb.UserResponse, error) {
log.Printf("Received: %v", in.GetId())
// здест к примеру логика получения пользователя из базы данных или другого сервиса
return &pb.UserResponse{Id: in.GetId(), Name: "John", Email: "john@example.com"}, nil
}

func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &server{})
log.Printf("Server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
Создадим клиент, который будет общаться с нашим сервером:

package main

import (
"context"
"log"
"os"
"time"

"google.golang.org/grpc"
pb "path/to/your/protos/user"
)

const (
address = "localhost:50051"
)

func main() {
// установка соединения с сервером
conn, err := grpc.Dial(address, grpc.WithInsecure(), grpc.WithBlock())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := pb.NewUserServiceClient(conn)

// ус тановим контекст с таймаутом
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

// запрс на получение пользователя
r, err := c.GetUser(ctx, &pb.UserRequest{Id: 123})
if err != nil {
log.Fatalf("could not greet: %v", err)
}
log.Printf("User: %s", r.GetName())
}

Protobuf это компактный, бинарный формат, который уменьшает размер передаваемых данных и ускоряет их обработку, также он совместим со многими ЯПами.

 
Сверху