
Привет, Хабр!
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/
Добавить комментарий