Сериализация данных в 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_RUNNING
Maps позволяет создавать структурированные коллекции пар ключ-значение.
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-инженерам на разных этапах развития карьеры. Регистрация доступна по ссылке.