Сегодня я хотел бы поделиться особенностью разработки сервисов на 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/
Добавить комментарий