Разрабатываем микросервисы на Golang + gRPC + gRPC Gateway

от автора

Сегодня я хотел бы поделиться особенностью разработки сервисов на Golang вместе с протоколом gRPC. В этой статья я не буду рассказывать, что такое gRPC, protobuf и для чего они нужны, вместо этого я сосредоточусь на технической части.

Мы напишем простое приложение на Golang, который в качестве транспортного протокола будет использовать gRPC, а так же с помощью gRPC Gateway мы подключим поддержку RESTful API. У нашего сервиса будет всего два ендпоинта, а именно:

  • Создать пользователя

  • Получить пользователя по идентификатору

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

user_model.proto
syntax = "proto3"; package com.example.user.model.v1; option go_package = "com.example/usersvcapi/v1";  message UserWrite {     string name = 1;     UserType type = 2; }  message UserRead {     string id = 1;     string name = 2;     UserType type = 3; }  enum UserType {     USER_TYPE_UNKNOWN = 0;     USER_TYPE_ADMIN = 1;     USER_TYPE_USER = 2; }

Я намеренно разделил модель пользователя, на запись и чтение, чтобы показать как на стороне сервиса мы можем сгенерировать уникальный идентификатор.

Обратите внимание на тип пользователя, который является перечислением. Перечисления в protobuf/syntax3 имеют ряд особенностей. Из интересного, например — нулевое значение должно быть первым элементом для совместимости с семантикой proto2, где первое значение перечисления всегда используется по умолчанию.

Так же, рекомендуется, чтобы имя элемента перечисления начиналось с типа перечисления + имя элемента. Например при следующем определении возникнет конфликт имен пространств элементов перечисления:

enum UserType {     UNKNOWN = 0;     ADMIN = 1;     USER = 2; }  enum UserGroup {     USER = 0; // Name conflict with UserType.USER     ADMIN = 1; // Name conflict with UserType.ADMIN }

Разобравшись с моделью, давайте перейдем к определию интерфейса сервиса:

user_service.proto
syntax = "proto3"; package com.example.user.service.v1; option go_package = "com.example/usersvcapi/v1";  import "user_model.proto"; import "google/api/annotations.proto";  service UserService {      rpc CreateUser(CreateUserRequest) returns (CreateUserResponse) {         option (google.api.http) = {             post: "/v1/users"             body: "user"         };     }      rpc GetUser(GetUserRequest) returns (GetUserResponse) {         option (google.api.http) = {             get: "/v1/users"         };     } }  message CreateUserRequest {     com.example.user.model.v1.UserWrite user = 1;     }  message CreateUserResponse {     string id = 1;     }  message GetUserRequest {     string id = 1; }  message GetUserResponse {     com.example.user.model.v1.UserRead user = 1; }

Теперь, когда мы описали интерфейс приложения, мы можем скомпилировать protobuf файлы под Golang. Для компиляции нам нужно установить следующие библиотеки:

$ go get -u google.golang.org/grpc $ go get -u github.com/golang/protobuf/protoc-gen-go $ sudo apt install protobuf-compiler $ mkdir consignment && cd consignment $ protoc -I=. --go_out=plugins=grpc:. consignment.proto

Другим вариантом, чтобы скомпилировать protobuf файлы под Golang, мы можем воспользоваться докер образом namely/protoc-all и тогда не нужно устанавливать дополнительные библиотеки. Опишем файл docker-compose:

version: "3.3" services:   protoc-all:     image: namely/protoc-all:latest     command:        -d proto       -o gen/pb-go       -i third_party/googleapis       -l go       --with-gateway     volumes:       - ./:/defs

Где:

  • -o — директория, куда будут скомпилированы proto stubs.

  • -i — путь к сторонним зависимостям, в нашем случае googleapis

  • -l — ЯП, в нашем случае Golang (go)

  • флаг —with-gateway, для генерации RESTful API

Когда protobuf файлы скомпилированы, мы можем приступить к написанию main файла, где собственно будет описан gRPC сервер.

main.go
func main() {  // Flags. // fs := flag.NewFlagSet("", flag.ExitOnError) grpcAddr := fs.String("grpc-addr", ":6565", "grpc address") httpAddr := fs.String("http-addr", ":8080", "http address") if err := fs.Parse(os.Args[1:]); err != nil { log.Fatal(err) }  // Setup gRPC servers. // baseGrpcServer := grpc.NewServer() userGrpcServer := NewUserGRPCServer() apiv1.RegisterUserServiceServer(baseGrpcServer, userGrpcServer)  // Setup gRPC gateway. // ctx := context.Background() rmux := runtime.NewServeMux() mux := http.NewServeMux() mux.Handle("/", rmux) { err := apiv1.RegisterUserServiceHandlerServer(ctx, rmux, userGrpcServer) if err != nil { log.Fatal(err) } }  // Serve. // var g run.Group { grpcListener, err := net.Listen("tcp", *grpcAddr) if err != nil { log.Fatal(err) } g.Add(func() error { log.Printf("Serving grpc address %s", *grpcAddr) return baseGrpcServer.Serve(grpcListener) }, func(error) { grpcListener.Close() }) } { httpListener, err := net.Listen("tcp", *httpAddr) if err != nil { log.Fatal(err) } g.Add(func() error { log.Printf("Serving http address %s", *httpAddr) return http.Serve(httpListener, mux) }, func(err error) { httpListener.Close() }) } { cancelInterrupt := make(chan struct{}) g.Add(func() error { c := make(chan os.Signal, 1) signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) select { case sig := <-c: return fmt.Errorf("received signal %s", sig) case <-cancelInterrupt: return nil } }, func(error) { close(cancelInterrupt) }) } if err := g.Run(); err != nil { log.Fatal(err) } }  type userServer struct { m map[string]*apiv1.UserWrite }  func NewUserGRPCServer() apiv1.UserServiceServer { return &userServer{ m: map[string]*apiv1.UserWrite{}, } }  func (s *userServer) CreateUser(ctx context.Context, req *apiv1.CreateUserRequest) (*apiv1.CreateUserResponse, error) { id, err := uuid.NewRandom() if err != nil { return nil, status.Error(codes.Internal, err.Error()) } s.m[id.String()] = req.User return &apiv1.CreateUserResponse{ Id: id.String(), }, nil }  func (s *userServer) GetUser(ctx context.Context, req *apiv1.GetUserRequest) (*apiv1.GetUserResponse, error) { foundUser, ok := s.m[req.Id] if !ok { return nil, status.Error(codes.NotFound, fmt.Errorf("User not found by id %v", req.Id).Error()) } return &apiv1.GetUserResponse{ User: &apiv1.UserRead{ Id:   req.Id, Name: foundUser.Name, Type: foundUser.Type, }, }, nil }

Запустим приложение и проверим как работают наши ендпоинты. Для тестирования RESTful API, вызовем следующие команды:

Создание пользователя
$ curl -d '{"name":"John", "type":1}' -H "Content-Type: application/json" -X POST http://localhost:8080/v1/users
Получение пользователя по ИД
$ curl -H "Content-Type: application/json" -X GET http://localhost:8080/v1/users?id=${USER_ID}

Для тестирования gRPC ендпоинтов, нужно будет воспользоваться BloomRPC.

Весь исходный код, доступен на github.


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


Комментарии

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

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