Сериализация данных в Golang с Protobuf
- суббота, 13 января 2024 г. в 00:00:18
 

Привет, Хабр!
Protobuf, или Protocol Buffers, это бинарный формат сериализации, разработанный в Google для эффективного обмена данными между сервисами. Это как JSON, только компактнее, быстрее и типизированнее. Если JSON был вашим первым крашем в мире сериализации, то Protobuf – это тот, с кем вы хотите серьёзных отношений.
Если вы хотите, чтобы ваши приложения общались между сервисами с молниеносной скоростью и минимальными задержками, тогда Protobuf — твой хороший выбор.
Первым делом, вам нужно скачать Protobuf Compiler. Загляните на GitHub страницу Protobuf и выберите версию, подходящую для вашей ОС.
После скачивания распакуйте содержимое и добавьте путь к исполняемому файлу protoc в вашу системную переменную PATH. В Unix это что-то вроде export PATH=$PATH:/path/to/protoc.
Запустите go get -u google.golang.org/protobuf/cmd/protoc-gen-go для установки плагина protoc для Go. Этот плагин нужен, чтобы превращать ваши .proto файлы в go-файлы.
Введите protoc --version в консоли. Если вы видите версию, вы успешно установили компилятор!
Прежде всего, .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; // Массив разделов обзора
}enum Status {
  UNKNOWN = 0;
  RUNNING = 1;
  STOPPED = 2;
}После компиляции .proto файла, мы получаем четко определенный тип Status, который можно использовать прямо в коде.
var currentStatus Status = Status_RUNNINGMaps позволяет создавать структурированные коллекции пар ключ-значение.
message Environment {
  map<string, string> variables = 1;
}После компиляции .proto файла, мы получаем map, который можно легко использовать.
env := Environment{
    Variables: map[string]string{"HOME": "/home/user", "PATH": "/usr/bin"},
}Прощай, массивы пар или сложные структуры для простых задач.
oneof - это инструмент в Protobuf для работы с различными структурами в одном поле.
message Command {
  oneof command_type {
    string text = 1;
    int32 number = 2;
  }
}После компиляции .proto, можно управлять этими полями как обычными структурами в Go.
cmd := Command{
    CommandType: &Command_Text{Text: "Hello, Protobuf!"},
}Допустим, у нас есть следующий .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 сервера и клиента.
Установим нужные пакеты:
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-инженерам на разных этапах развития карьеры. Регистрация доступна по ссылке.