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

—

от автора

Привет, Хабр!

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 это компактный, бинарный формат, который уменьшает размер передаваемых данных и ускоряет их обработку, также он совместим со многими ЯПами.

В завершение хочу порекомендовать бесплатный вебинар, где вы узнаете, какие конкретные компетенции необходимы Golang-инженерам на разных этапах развития карьеры. Регистрация доступна по ссылке.


ссылка на оригинал статьи https://habr.com/ru/articles/784732/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *