Microservice Applications on GoMicro

Due to its compilation capabilities and built-in mechanisms for concurrent multitasking, Go is very well suited for building network applications and is actively used in building tools for DevOps and distributed applications. In this article, we will look at some of the features of the GoMicro framework for implementing microservice applications in Go.

In a microservice architecture, one of the most important aspects is the mechanism of data transfer within a distributed application, and GoMicro supports both the use of REST and the ability to use message queues for data exchange in both the RPC approach and the implementation of the architecture of reactive event-based applications. GoMicro is based on an extensible architecture and provides a large number of important subsystems for implementing microservices:

  • Authentication and authorization provide a per-service identifier and certificates, and implement rule-based access control.

  • Data store – a simple data warehouse interface for reading, writing and deleting records. Data can be saved both in memory and external storage.

  • Dynamic Configuration – Download and update in real time dynamic configuration from anywhere. The configuration interface provides a way to load application-level configuration from any source, such as environment variables, a file, etc. The sources can be combined and you can specify how to fallback to a different source or specify default values.

  • Service discovery mechanism – automatic registration of services and name resolution (default based on mDNS).

  • Load balancing – client-side load balancing based on service discovery. The load is distributed evenly among several implementations of services with automatic switching in the presence of errors.

  • Message Encoding – the ability to encode messages in JSON / Protobuf.

  • RPC client/server – RPC-based request/response with support for bi-directional streaming.

  • Asynchronous messaging – PubSub is the basis for distributed applications driven by events (based on HTTP requests).

  • Event Streaming implements support for NATS Jetstream and Redis streams.

  • Synchronization. Distributed locking and a protocol for dynamic leader election among the available quorum are supported.

  • Pluggable interfaces – Go Micro uses Go interfaces for each abstraction of a distributed system and allows you to create your own implementations for each component.

To connect GoMicro, you need to install and import the module from “go-micro.dev/v4” and then use its methods to control the created service. Also available in modules at github.com/go-micro/plugins/v4/… are plugins that fall into the following categories:

  • broker – interaction with message brokers to implement message streaming (NATS, RabbitMQ, Kafka)

  • client – clients for requests to other microservices (via RPC, gRPC or HTTP)

  • server – server component to provide access to the microservice (RPC, gRPC, HTTP)

  • codec – message encoding in BSON, Mercury, Protobuf

  • config – distributed configuration management

  • registry – registration and discovery of services (including, it can interact with Kubernetes)

  • selector – load balancing

  • transportation – two-way data transfer via NATS or RabbitMQ

  • wrapper – additional middleware (for example, logging, request tracing, request rate limiting, etc.)

The creation of a service begins with a call to NewService and further initialization of the created structure. You can also immediately register the service and configure ways to detect other services, connect server and client components.

package main

import (
	"go-micro.dev/v4"
  	"github.com/go-micro/plugins/v4/registry/kubernetes"
  	grpcc "github.com/go-micro/plugins/v4/client/grpc"
	grpcs "github.com/go-micro/plugins/v4/server/grpc"
)

func main() {
    registry := kubernetes.NewRegistry()
	service := micro.NewService(
		micro.Name("my.service"),
        micro.Registry(registry),
        micro.Server(grpcs.NewServer()),
		micro.Client(grpcc.NewClient()),
	)
    service.Server().handle()
	service.Init()
    service.Run()
}

Further, code generation mechanisms for protobuf (protoc-gen-go) can be used to implement handlers and calls to remote methods, methods in service are used to access registered components. For example, to access the server object, you can call service.Server(), similarly to connect to the generated client service.Client(), and so on. With a proto file and a generated description of the methods and protocol for interacting with the microservice, the call may look like this (the structure generated by protoc-gen-go is imported into pb)

	if err := pb.RegisterPaymentServiceHandler(srv.Server(), new(handler.PaymentService)); err != nil {
		logger.Fatal(err)
	}

At the same time, the proto-file itself describes all aspects of interaction with the microservice and defines additional data types (enumerations, structures, etc.), for example:

syntax = "proto3";

package shop;

option go_package = "./proto;shop";

service Health {
  rpc Check(HealthCheckRequest) returns (HealthCheckResponse) {}
  rpc Watch(HealthCheckRequest) returns (stream HealthCheckResponse) {}
}

message HealthCheckRequest { 
	string service = 1;
}

message HealthCheckResponse {
  enum ServingStatus {
    UNKNOWN = 0;
    SERVING = 1;
    NOT_SERVING = 2;
    SERVICE_UNKNOWN = 3;
  }
  ServingStatus status = 1;
}

When using service registries, you can do additional processing (for example, create implementations of selectors to find suitable services). It is also quite simple to integrate message queues:

package main

import (
	"fmt"

	"context"
	"go-micro.dev/v4"
)

func pub(i int, p micro.Publisher) {
	msg := &Message{
		Say: fmt.Sprintf("This is an async message %d", i),
	}

	if err := p.Publish(context.TODO(), msg); err != nil {
		fmt.Println("pub err: ", err)
		return
	}

	fmt.Printf("Published %d: %v\n", i, msg)
}

func main() {
	service := micro.NewService()
	service.Init()

	p := micro.NewPublisher("example", service.Client())

	for i := 0; i < 10; i++ {
		pub(i, p)
	}
}

Similarly, you can create distributed configurations, discover services (via the registry), create RPC clients, subscribe to the appearance of messages in RabbitMQ/Kafka queues. Mechanisms are also available for configuring heartbeat (to check the availability and correct functioning of services):

	service := micro.NewService(
		micro.Name("payment"),
  		micro.RegisterInterval(time.Second*30),
		micro.RegisterTTL(time.Second*120),
	)

Additionally, you can connect your own extensions using micro.WrapHandler:

import (
    "log"
	"go-micro.dev/v4"
	"go-micro.dev/v4/server"
)

func logWrapper(fn server.HandlerFunc) server.HandlerFunc {
	return func(ctx context.Context, req server.Request, rsp interface{}) error {
		log.Printf("[request]: %v", req.Endpoint())
		err := fn(ctx, req, rsp)
		return err
	}
}

func main() {
	service := micro.NewService(
		micro.Name("greeter"),
		// wrap the handler
		micro.WrapHandler(logWrapper),
	)

	service.Init()

	proto.RegisterHandler(service.Server(), new(PaymentService))

	if err := service.Run(); err != nil {
		fmt.Println(err)
	}
}

To monitor running microservices, the GoMicro Dashboard can be used, which can be installed via go install github.com/go-micro/dashboard@latest.

Thus, MicroGo provides a lightweight and extensible framework for developing microservices, which provides the developed components with the basic capabilities for securely accessing and discovering other microservices, data storage, interaction via HTTP / gRPC or message queues, and other important aspects for implementing a microservice architecture.

In conclusion, I invite you to free lessonwhere we will look at the pros and cons of monoliths and microservices, as well as the main patterns in microservice architecture.

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *