Construyendo una arquitectura de microservicios en Golang y gRPC, Parte 1

Introducción a la arquitectura de microservicios


Parte 1 de 10


Adaptación de los artículos de Ewan Valentine.


Esta es una serie de diez partes, intentaré escribir sobre la creación de microservicios en Golang una vez al mes. Usaré protobuf y gRPC como protocolo principal de transporte.


La pila que utilicé: golang, mongodb, grpc, docker, Google Cloud, Kubernetes, NATS, CircleCI, Terraform y go-micro.


¿Por qué necesito esto? Como me llevó mucho tiempo resolverlo y resolver los problemas acumulados. También quería compartir con ustedes lo que aprendí sobre la creación, prueba e implementación de microservicios en Go y otras nuevas tecnologías.


En esta parte, quiero mostrar los conceptos básicos y las tecnologías para construir microservicios. Escribamos una implementación simple. El proyecto tendrá las siguientes entidades:


  • carga
  • inventario
  • juicio
  • los usuarios
  • los roles
  • autenticación


Para ir más allá, debe instalar Golang y las bibliotecas necesarias, así como crear un repositorio git.


Teoría


¿Qué es la arquitectura de microservicios?


Los microservicios aíslan una funcionalidad separada en un servicio, autosuficiente en términos de la función realizada por este servicio. Por compatibilidad con otros servicios, tiene una interfaz bien conocida y predefinida.
Los microservicios se comunican entre sí mediante mensajes transmitidos a través de algún intermediario, intermediario de mensajes.



Gracias a la arquitectura de microservicios, la aplicación no se puede escalar en su totalidad, sino en partes. Por ejemplo, si el servicio de autorización "se contrae" con más frecuencia que otros, podemos aumentar el número de instancias. Este concepto está en línea con los conceptos de computación en la nube y contenedorización en general.


Por que golang


Los microservicios son compatibles en casi todos los idiomas, después de todo, los microservicios son un concepto, no una estructura o herramienta específica. Sin embargo, algunos idiomas son más adecuados y, además, tienen un mejor soporte para microservicios que otros. Un idioma con gran soporte es Golang.


Conoce a protobuf / gRPC


Como se mencionó anteriormente, los microservicios se dividen en bases de código separadas, uno de los problemas importantes asociados con los microservicios es la comunicación. Si tiene un monolito, simplemente puede llamar al código directamente desde otro lugar de su programa.


Para resolver el problema de comunicación, podemos usar el enfoque REST tradicional y transferir datos en formato JSON o XML a través de HTTP. Pero este enfoque tiene sus inconvenientes, por ejemplo, que antes de enviar un mensaje debe codificar sus datos y descodificarlos en el lado receptor. Y esto es una sobrecarga y aumenta la complejidad del código.


¡Hay una solución! Este es el protocolo gRPC : un protocolo liviano basado en binarios que elimina la transmisión de encabezados HTTP, y esto nos ahorrará algunos bytes. El futuro HTTP2 también implica el uso de datos binarios, que nuevamente hablan a favor de gRPC. HTTP2 permite la comunicación bidireccional, ¡y es increíble!


GRPC también le permite definir la interfaz de su servicio en un formato amigable: esto es> protobuf .


Practica


Cree el archivo /project/consigment.proto.
Documentación oficial de protobuf


consigment.proto
//consigment.proto syntax = "proto3"; package go.micro.srv.consignment; service ShippingService { rpc CreateConsignment(Consignment) returns (Response) {} } message Consignment { string id = 1; string description = 2; int32 weight = 3; repeated Container containers = 4; string vessel_id = 5; } message Container { string id = 1; string customer_id = 2; string origin = 3; string user_id = 4; } message Response { bool created = 1; Consignment consignment = 2; } 

Este es un ejemplo simple que contiene el servicio que desea proporcionar a otros servicios: servicio ShippingService, luego definiremos nuestros mensajes. Protobuf es un protocolo de tipo estático, y podemos crear tipos personalizados (similares a las estructuras en golang). Aquí el contenedor está anidado en el lote.


Instale las bibliotecas, el compilador y compile nuestro protocolo:


 $ 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 

El resultado debe ser un archivo:


consignment.pb.go
 // Code generated by protoc-gen-go. DO NOT EDIT. // source: consignment.proto package consignment import ( fmt "fmt" proto "github.com/golang/protobuf/proto" context "golang.org/x/net/context" grpc "google.golang.org/grpc" math "math" ) // Reference imports to suppress errors if they are not otherwise used. var _ = proto.Marshal var _ = fmt.Errorf var _ = math.Inf // This is a compile-time assertion to ensure that this generated file // is compatible with the proto package it is being compiled against. // A compilation error at this line likely means your copy of the // proto package needs to be updated. const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package type Consignment struct { Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` Description string `protobuf:"bytes,2,opt,name=description,proto3" json:"description,omitempty"` Weight int32 `protobuf:"varint,3,opt,name=weight,proto3" json:"weight,omitempty"` Containers []*Container `protobuf:"bytes,4,rep,name=containers,proto3" json:"containers,omitempty"` VesselId string `protobuf:"bytes,5,opt,name=vessel_id,json=vesselId,proto3" json:"vessel_id,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Consignment) Reset() { *m = Consignment{} } func (m *Consignment) String() string { return proto.CompactTextString(m) } func (*Consignment) ProtoMessage() {} func (*Consignment) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{0} } func (m *Consignment) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Consignment.Unmarshal(m, b) } func (m *Consignment) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Consignment.Marshal(b, m, deterministic) } func (m *Consignment) XXX_Merge(src proto.Message) { xxx_messageInfo_Consignment.Merge(m, src) } func (m *Consignment) XXX_Size() int { return xxx_messageInfo_Consignment.Size(m) } func (m *Consignment) XXX_DiscardUnknown() { xxx_messageInfo_Consignment.DiscardUnknown(m) } var xxx_messageInfo_Consignment proto.InternalMessageInfo func (m *Consignment) GetId() int32 { if m != nil { return m.Id } return 0 } func (m *Consignment) GetDescription() string { if m != nil { return m.Description } return "" } func (m *Consignment) GetWeight() int32 { if m != nil { return m.Weight } return 0 } func (m *Consignment) GetContainers() []*Container { if m != nil { return m.Containers } return nil } func (m *Consignment) GetVesselId() string { if m != nil { return m.VesselId } return "" } type Container struct { Id int32 `protobuf:"varint,1,opt,name=id,proto3" json:"id,omitempty"` CustomerId string `protobuf:"bytes,2,opt,name=customer_id,json=customerId,proto3" json:"customer_id,omitempty"` Origin string `protobuf:"bytes,3,opt,name=origin,proto3" json:"origin,omitempty"` UserId string `protobuf:"bytes,4,opt,name=user_id,json=userId,proto3" json:"user_id,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Container) Reset() { *m = Container{} } func (m *Container) String() string { return proto.CompactTextString(m) } func (*Container) ProtoMessage() {} func (*Container) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{1} } func (m *Container) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Container.Unmarshal(m, b) } func (m *Container) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Container.Marshal(b, m, deterministic) } func (m *Container) XXX_Merge(src proto.Message) { xxx_messageInfo_Container.Merge(m, src) } func (m *Container) XXX_Size() int { return xxx_messageInfo_Container.Size(m) } func (m *Container) XXX_DiscardUnknown() { xxx_messageInfo_Container.DiscardUnknown(m) } var xxx_messageInfo_Container proto.InternalMessageInfo func (m *Container) GetId() int32 { if m != nil { return m.Id } return 0 } func (m *Container) GetCustomerId() string { if m != nil { return m.CustomerId } return "" } func (m *Container) GetOrigin() string { if m != nil { return m.Origin } return "" } func (m *Container) GetUserId() string { if m != nil { return m.UserId } return "" } type Response struct { Created bool `protobuf:"varint,1,opt,name=created,proto3" json:"created,omitempty"` Consignment *Consignment `protobuf:"bytes,2,opt,name=consignment,proto3" json:"consignment,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } func (m *Response) Reset() { *m = Response{} } func (m *Response) String() string { return proto.CompactTextString(m) } func (*Response) ProtoMessage() {} func (*Response) Descriptor() ([]byte, []int) { return fileDescriptor_3804bf87090b51a9, []int{2} } func (m *Response) XXX_Unmarshal(b []byte) error { return xxx_messageInfo_Response.Unmarshal(m, b) } func (m *Response) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { return xxx_messageInfo_Response.Marshal(b, m, deterministic) } func (m *Response) XXX_Merge(src proto.Message) { xxx_messageInfo_Response.Merge(m, src) } func (m *Response) XXX_Size() int { return xxx_messageInfo_Response.Size(m) } func (m *Response) XXX_DiscardUnknown() { xxx_messageInfo_Response.DiscardUnknown(m) } var xxx_messageInfo_Response proto.InternalMessageInfo func (m *Response) GetCreated() bool { if m != nil { return m.Created } return false } func (m *Response) GetConsignment() *Consignment { if m != nil { return m.Consignment } return nil } func init() { proto.RegisterType((*Consignment)(nil), "Consignment") proto.RegisterType((*Container)(nil), "Container") proto.RegisterType((*Response)(nil), "Response") } func init() { proto.RegisterFile("consignment.proto", fileDescriptor_3804bf87090b51a9) } var fileDescriptor_3804bf87090b51a9 = []byte{ // 281 bytes of a gzipped FileDescriptorProto 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x91, 0xbf, 0x4e, 0x33, 0x31, 0x10, 0xc4, 0xbf, 0xcb, 0xff, 0x5b, 0x7f, 0x02, 0xc5, 0x05, 0x58, 0x50, 0x70, 0xba, 0x2a, 0xa2, 0x70, 0x11, 0x9e, 0x00, 0xa5, 0x4a, 0xeb, 0xd0, 0xa3, 0x60, 0xaf, 0x2e, 0x2b, 0x11, 0xfb, 0x64, 0x3b, 0xe1, 0x75, 0x78, 0x54, 0x74, 0xbe, 0x1c, 0x18, 0x51, 0xce, 0xac, 0x67, 0xf7, 0xa7, 0x31, 0x2c, 0xb5, 0xb3, 0x81, 0x1a, 0x7b, 0x44, 0x1b, 0x65, 0xeb, 0x5d, 0x74, 0xf5, 0x67, 0x01, 0x6c, 0xf3, 0xe3, 0xf2, 0x2b, 0x18, 0x91, 0x11, 0x45, 0x55, 0xac, 0xa6, 0x6a, 0x44, 0x86, 0x57, 0xc0, 0x0c, 0x06, 0xed, 0xa9, 0x8d, 0xe4, 0xac, 0x18, 0x55, 0xc5, 0xaa, 0x54, 0xb9, 0xc5, 0x6f, 0x60, 0xf6, 0x81, 0xd4, 0x1c, 0xa2, 0x18, 0xa7, 0xd4, 0x45, 0xf1, 0x47, 0x00, 0xed, 0x6c, 0xdc, 0x93, 0x45, 0x1f, 0xc4, 0xa4, 0x1a, 0xaf, 0xd8, 0x1a, 0xe4, 0x66, 0xb0, 0x54, 0x36, 0xe5, 0xf7, 0x50, 0x9e, 0x31, 0x04, 0x7c, 0x7f, 0x25, 0x23, 0xa6, 0xe9, 0xc6, 0xa2, 0x37, 0xb6, 0xa6, 0x3e, 0x42, 0xf9, 0x9d, 0xfa, 0xc3, 0xf7, 0x00, 0x4c, 0x9f, 0x42, 0x74, 0x47, 0xf4, 0x5d, 0xb6, 0xe7, 0x83, 0xc1, 0xda, 0x9a, 0x0e, 0xcf, 0x79, 0x6a, 0xc8, 0x26, 0xbc, 0x52, 0x5d, 0x14, 0xbf, 0x85, 0xf9, 0x29, 0xf4, 0xa1, 0x49, 0x3f, 0xe8, 0xe4, 0xd6, 0xd4, 0x2f, 0xb0, 0x50, 0x18, 0x5a, 0x67, 0x03, 0x72, 0x01, 0x73, 0xed, 0x71, 0x1f, 0xb1, 0x3f, 0xb9, 0x50, 0x83, 0xe4, 0x12, 0x58, 0x56, 0x66, 0xba, 0xcb, 0xd6, 0xff, 0x65, 0x56, 0xa5, 0xca, 0x1f, 0xac, 0x9f, 0xe1, 0x7a, 0x77, 0xa0, 0xb6, 0x25, 0xdb, 0xec, 0xd0, 0x9f, 0x49, 0x23, 0x97, 0xb0, 0xdc, 0xa4, 0x6d, 0x79, 0xff, 0xbf, 0x56, 0xdc, 0x95, 0x72, 0x40, 0xa9, 0xff, 0xbd, 0xcd, 0xd2, 0x8f, 0x3d, 0x7d, 0x05, 0x00, 0x00, 0xff, 0xff, 0x84, 0x5c, 0xa4, 0x06, 0xc6, 0x01, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. var _ context.Context var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. const _ = grpc.SupportPackageIsVersion4 // ShippingServiceClient is the client API for ShippingService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://godoc.org/google.golang.org/grpc#ClientConn.NewStream. type ShippingServiceClient interface { CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) } type shippingServiceClient struct { cc *grpc.ClientConn } func NewShippingServiceClient(cc *grpc.ClientConn) ShippingServiceClient { return &shippingServiceClient{cc} } func (c *shippingServiceClient) CreateConsignment(ctx context.Context, in *Consignment, opts ...grpc.CallOption) (*Response, error) { out := new(Response) err := c.cc.Invoke(ctx, "/ShippingService/CreateConsignment", in, out, opts...) if err != nil { return nil, err } return out, nil } // ShippingServiceServer is the server API for ShippingService service. type ShippingServiceServer interface { CreateConsignment(context.Context, *Consignment) (*Response, error) } func RegisterShippingServiceServer(s *grpc.Server, srv ShippingServiceServer) { s.RegisterService(&_ShippingService_serviceDesc, srv) } func _ShippingService_CreateConsignment_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(Consignment) if err := dec(in); err != nil { return nil, err } if interceptor == nil { return srv.(ShippingServiceServer).CreateConsignment(ctx, in) } info := &grpc.UnaryServerInfo{ Server: srv, FullMethod: "/ShippingService/CreateConsignment", } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(ShippingServiceServer).CreateConsignment(ctx, req.(*Consignment)) } return interceptor(ctx, in, info, handler) } var _ShippingService_serviceDesc = grpc.ServiceDesc{ ServiceName: "ShippingService", HandlerType: (*ShippingServiceServer)(nil), Methods: []grpc.MethodDesc{ { MethodName: "CreateConsignment", Handler: _ShippingService_CreateConsignment_Handler, }, }, Streams: []grpc.StreamDesc{}, Metadata: "consignment.proto", } 

Si, entonces algo salió mal. Preste atención a los argumentos -I es la ruta donde el compilador está buscando archivos, --go_out donde se creará un nuevo archivo. Siempre hay ayuda


 $ protoc -h 

Este es el código generado automáticamente por las bibliotecas gRPC / protobuf para que pueda asociar su definición de protobuf con su propio código.


Escribiremos main.go


main.go
 package seaport import ( "log" "net" //    pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" "google.golang.org/grpc/reflection" ) const ( port = ":50051" ) //IRepository -   type IRepository interface { Create(*pbf.Consignment) (*pbf.Consignment, error) } // Repository -    , //        type Repository struct { consignments []*pbf.Consignment } //Create -    func (repo *Repository) Create(consignment *pbf.Consignment) (*pbf.Consignment, error) { updated := append(repo.consignments, consignment) repo.consignments = updated return consignment, nil } //         //       .     //         . . type service struct { repo IRepository } // CreateConsignment -        , //    create,      //     gRPC. func (s *service) CreateConsignment(ctx context.Context, req *pbf.Consignment) (*pbf.Response, error) { //      consignment, err := s.repo.Create(req) if err != nil { return nil, err } //   `Response`, //        return &pbf.Response{Created: true, Consignment: consignment}, nil } func main() { repo := &Repository{} //   gRPC    tcp lis, err := net.Listen("tcp", port) if err != nil { log.Fatalf("failed to listen: %v", err) } s := grpc.NewServer() //      gRPC,    //        //  `Response`,       pbf.RegisterShippingServiceServer(s, &service{repo}) //      gRPC. reflection.Register(s) if err := s.Serve(lis); err != nil { log.Fatalf("failed to serve: %v", err) } } 

Lea atentamente los comentarios que quedan en el código. Aparentemente, aquí estamos creando una lógica de implementación en la cual nuestros métodos gRPC interactúan usando los formatos generados, creando un nuevo servidor gRPC en el puerto 50051. Ahora nuestro servicio gRPC vivirá allí.
Puede ejecutar esto con $ go run main.go , pero no verá nada y no podrá usarlo ... Entonces, creemos un cliente para verlo en acción.


Creemos una interfaz de línea de comando que tome un archivo JSON e interactúe con nuestro servicio gRPC.


En el directorio raíz, cree un nuevo subdirectorio de $ mkdir consignment-cli . En este directorio, cree un archivo cli.go con los siguientes contenidos:


cli.go
 package main import ( "encoding/json" "io/ioutil" "log" "os" pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) //    func parseFile(file string) (*pbf.Consignment, error) { var consignment *pbf.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { //     conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("  : %v", err) } defer conn.Close() client := pbf.NewShippingServiceClient(conn) //    consignment.json, //          file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("   : %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("  : %v", err) } log.Printf(": %t", r.Created) } 

Ahora cree un lote (consignment-cli / consignment.json):


 { "description": "  ", "weight": 100, "containers": [ { "customer_id": "_001", "user_id": "_001", "origin": " " } ], "vessel_id": "_001" } 

Ahora, si ejecuta $ go, ejecute main.go desde el paquete de puerto y luego ejecute $ go, ejecute cli.go en un panel de terminales separado. Debería ver el mensaje "Creado: verdadero".
Pero, ¿cómo podemos verificar qué se creó exactamente? Actualicemos nuestro servicio utilizando el método GetConsignments para que podamos ver todos nuestros lotes creados.


consigment.proto
 //consigment.proto syntax = "proto3"; service ShippingService{ rpc CreateConsignment(Consignment) returns (Response) {} //    rpc GetConsignments(GetRequest) returns (Response) {} } message Consignment { int32 id = 1; string description = 2; int32 weight = 3; repeated Container containers = 4; string vessel_id = 5; } message Container { int32 id =1; string customer_id =2; string origin = 3; string user_id = 4; } //    message GetRequest {} message Response { bool created = 1; Consignment consignment = 2; //     //     repeated Consignment consignments = 3; } 

Entonces, aquí creamos un nuevo método en nuestro servicio llamado GetConsignments , también creamos un nuevo GetRequest , que aún no contiene nada. También agregamos un campo de lotes enviados a nuestro mensaje de respuesta. Notará que el tipo aquí tiene la palabra clave repetida hasta el tipo. Esto, como probablemente haya adivinado, simplemente significa tratar este campo como una matriz de estos tipos.


No se apresure a ejecutar el programa, la implementación de nuestros métodos gRPC se basa en hacer coincidir la interfaz creada por la biblioteca protobuf, debemos asegurarnos de que nuestra implementación coincida con nuestra definición de proto.


 //seaport/main.go //IRepository -   type IRepository interface { Create(*pbf.Consignment) (*pbf.Consignment, error) GetAll() []*pbf.Consignment } //GetAll -       func (repo *Repository) GetAll() []*pbf.Consignment { return repo.consignments } //GetConsignments -         func (s *service) GetConsignments(ctx context.Context, req *pbf.GetRequest) (*pbf.Response, error) { consignments := s.repo.GetAll() return &pbf.Response{Consignments: consignments}, nil } 

Aquí hemos incluido nuestro nuevo método GetConsignments, actualizado nuestro repositorio e interfaz, respectivamente creados en la definición consignments.proto. Si ejecuta $ go run main.go nuevamente , el programa debería funcionar nuevamente.


Actualicemos nuestra herramienta cli para incluir la capacidad de llamar a este método y es posible enumerar nuestras partes:


cli.go
 package main import ( "encoding/json" "io/ioutil" "log" "os" pbf "seaport/consignment" "golang.org/x/net/context" "google.golang.org/grpc" ) const ( address = "localhost:50051" defaultFilename = "consignment.json" ) //    func parseFile(file string) (*pbf.Consignment, error) { var consignment *pbf.Consignment data, err := ioutil.ReadFile(file) if err != nil { return nil, err } json.Unmarshal(data, &consignment) return consignment, err } func main() { //     conn, err := grpc.Dial(address, grpc.WithInsecure()) if err != nil { log.Fatalf("  : %v", err) } defer conn.Close() client := pbf.NewShippingServiceClient(conn) //    consignment.json, //          file := defaultFilename if len(os.Args) > 1 { file = os.Args[1] } consignment, err := parseFile(file) if err != nil { log.Fatalf("   : %v", err) } r, err := client.CreateConsignment(context.Background(), consignment) if err != nil { log.Fatalf("  : %v", err) } log.Printf(": %t", r.Created) getAll, err := client.GetConsignments(context.Background(), &pbf.GetRequest{}) if err != nil { log.Fatalf("    : %v", err) } for _, cns := range getAll.Consignments { fmt.Printf("Id: %v\n", cns.GetId()) fmt.Printf("Description: %v\n", cns.GetDescription()) fmt.Printf("Weight: %d\n", cns.GetWeight()) fmt.Printf("VesselId: %v\n", cns.GetVesselId()) for _, cnt := range cns.GetContainers() { fmt.Printf("\tId: %v\n", cnt.GetId()) fmt.Printf("\tUserId: %v\n", cnt.GetUserId()) fmt.Printf("\tCustomerId: %v\n", cnt.GetCustomerId()) fmt.Printf("\tOrigin: %v\n", cnt.GetOrigin()) } } } 

Agregue el código anterior a cli.go y ejecute $ go ejecute cli.go nuevamente . El cliente ejecutará CreateConsignment y luego llamará a GetConsignments. Y debería ver que en la lista de respuestas contiene la composición de la fiesta.


Por lo tanto, tenemos el primer microservicio y cliente para interactuar con él usando protobuf y gRPC.


La siguiente parte de esta serie incluirá la integración go-micro, que es una base poderosa para crear microservicios basados ​​en gRPC. También crearemos nuestro segundo servicio. Considere el trabajo de nuestros servicios en contenedores Docker, en la siguiente parte de esta serie de artículos.

Source: https://habr.com/ru/post/es425653/


All Articles