DEV Community

Cover image for Go + gRPC with Go kit
Junerey Casuga
Junerey Casuga

Posted on

Go + gRPC with Go kit

Building applications with Go can be sometimes intimidating especially when you’re just starting to learn it. Let alone transporting your APIs through gRPC. In this post, I will guide you on writing a simple Go microservice using gRPC with the help of Go kit.

What is Go kit? and why?

Go kit is the brain child of Peter Bourgon which he talked about during a FOSDEM and the Google Campus London meetup in 2015. As what he has envisioned, Go kit is a toolkit for building microservices in Go.

It provides you a set of standard libraries (packages) that are considered essentials when building microservices so you can focus on what’s important, your application’s business logic. Here is a list of packages Go kit provides

  • Authentication (Basic/Casbin/JWT)
  • Circuit Breaker (Hystrix/GoBreaker/HandyBreaker)
  • Logging: Provides an interface for structured logging.
  • Metrics: Provides an interface for service instrumentation. Has adapters for CloudWatch, Prometheus, Graphite, and more.
  • Rate Limiting
  • Service Discovery Utilities
  • Tracing (OpenCensus, OpenTracing, Zipkin)
  • Transport (HTTP/gRPC/ NATS/AWS Lambda, and more)

Other than being a toolkit, it also encourages good design principles for your application such as SOLID design principles, Domain-Driven Design (DDD), and Hexagonal architecture.

Go kit's concept

Go kit's concept is based on three major components:

  • Services
  • Endpoints
  • Transports Alt Text

Services (Business Logic)

This is where the core business logic of your API is located. In Go kit, each service method are converted into an Endpoint.

Endpoints

An endpoint represents a single RPC method. Each Service method converts into an Endpoint to make RPC style communication between servers and clients.

Transports

In a microservice architecture, a microservice often communicate through transports such as HTTP or gRPC. A single endpoint can be exposed by multiple transports.

Let's build our microservice

For our example microservice, we will build a very simple function that would get the sum of two numbers. Before we get started, make sure that you have Protocol buffer and Go plugins for the protocol compiler installed.

To give you an overview what our project structure would look like by the end of this article, you can refer to the directory tree below.

.
├── cmd
│   └── main.go         # main entrypoint file          
├── endpoints
│   └── endpoints.go    # contains the endpoint definition
├── pb
│   ├── math.pb.go      # our gRPC generated code
│   └── math.proto      # our protobuf definitions
├── service
│   └── api.go          # contains the service's core business logic
└── transports
    └── grpc.go         # contains the gRPC transport
Enter fullscreen mode Exit fullscreen mode

Protobuf

First of, we would need to create our Protobuf file inside our pb directory. Since you’re reading this, I would assume that you already know the basics of Protobuf; and the code snippet below should be pretty straightforward to understand 😄

syntax = "proto3";

option go_package = "github.com/junereycasuga/gokit-grpc-demo/pb";

service MathService {
  rpc Add(MathRequest) returns (MathResponse) {}
}

message MathRequest {
  float numA = 1;
  float numB = 2;
}

message MathResponse {
  float result = 1;
}
Enter fullscreen mode Exit fullscreen mode

Once we have our Protobuf file set, we would then need to generate our gRPC code using the following command.

protoc --go_out=plugins=grpc:. pb/math.proto
Enter fullscreen mode Exit fullscreen mode

Service

Next up, we will create the core business logic of our microservice. So in service/api.go , we would add our service definition by using the following code below.

package service

import (
    "context"

    "github.com/go-kit/kit/log"
)

type service struct {
    logger log.Logger
}

// Service interface describes a service that adds numbers
type Service interface {
    Add(ctx context.Context, numA, numB float32) (float32, error)
}

// NewService returns a Service with all of the expected dependencies
func NewService(logger log.Logger) Service {
    return &service{
        logger: logger,
    }
}

// Add func implements Service interface
func (s service) Add(ctx context.Context, numA, numB float32) (float32, error) {
    return numA + numB, nil
}
Enter fullscreen mode Exit fullscreen mode

Endpoint

package endpoints

import (
    "context"

    "github.com/go-kit/kit/endpoint"
    "github.com/junereycasuga/gokit-grpc-demo/service"
)

// Endpoints struct holds the list of endpoints definition
type Endpoints struct {
    Add endpoint.Endpoint
}

// MathReq struct holds the endpoint request definition
type MathReq struct {
    NumA float32
    NumB float32
}

// MathResp struct holds the endpoint response definition
type MathResp struct {
    Result float32
}

// MakeEndpoints func initializes the Endpoint instances
func MakeEndpoints(s service.Service) Endpoints {
    return Endpoints{
        Add: makeAddEndpoint(s),
    }
}

func makeAddEndpoint(s service.Service) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (response interface{}, err error) {
        req := request.(MathReq)
        result, _ := s.Add(ctx, req.NumA, req.NumB)
        return MathResp{Result: result}, nil
    }
}
Enter fullscreen mode Exit fullscreen mode

From the code above for endpoints/endpoints.go, we have the Endpoints struct that serves as a helper struct to collect all of the endpoints into a single parameter.

The MathReq and MathResp simply defines the request parameters and response values for the Add method.

makeAddEndpoint func constructs our Add endpoint wrapping the service.

And finally, MakeEndpoints func returns an Endpoints that wraps the provided server and wires in all of the expected endpoints.

Transport

package transport

import (
    "context"

    "github.com/go-kit/kit/log"
    gt "github.com/go-kit/kit/transport/grpc"
    "github.com/junereycasuga/gokit-grpc-demo/endpoints"
    "github.com/junereycasuga/gokit-grpc-demo/pb"
)

type gRPCServer struct {
    add gt.Handler
}

// NewGRPCServer initializes a new gRPC server
func NewGRPCServer(endpoints endpoints.Endpoints, logger log.Logger) pb.MathServiceServer {
    return &gRPCServer{
        add: gt.NewServer(
            endpoints.Add,
            decodeMathRequest,
            encodeMathResponse,
        ),
    }
}

func (s *gRPCServer) Add(ctx context.Context, req *pb.MathRequest) (*pb.MathResponse, error) {
    _, resp, err := s.add.ServeGRPC(ctx, req)
    if err != nil {
        return nil, err
    }
    return resp.(*pb.MathResponse), nil
}

func decodeMathRequest(_ context.Context, request interface{}) (interface{}, error) {
    req := request.(*pb.MathRequest)
    return endpoints.MathReq{NumA: req.NumA, NumB: req.NumB}, nil
}

func encodeMathResponse(_ context.Context, response interface{}) (interface{}, error) {
    resp := response.(endpoints.MathResp)
    return &pb.MathResponse{Result: resp.Result}, nil
}
Enter fullscreen mode Exit fullscreen mode

In our code for transports/grpc.go, we have NewGRPCServer which makes a set of endpoints available as a gRPC MathServiceServer.

Main entrypoint

This is where we stitch things together. Here we initiate our services, endpoints and our gRPC server, then we serve it using google.golang.org/grpc package.

package main

import (
    "fmt"
    "net"
    "os"
    "os/signal"
    "syscall"

    "github.com/go-kit/kit/log"
    "github.com/go-kit/kit/log/level"
    "github.com/junereycasuga/gokit-grpc-demo/endpoints"
    "github.com/junereycasuga/gokit-grpc-demo/pb"
    "github.com/junereycasuga/gokit-grpc-demo/service"
    transport "github.com/junereycasuga/gokit-grpc-demo/transports"
    "google.golang.org/grpc"
)

func main() {
    var logger log.Logger
    logger = log.NewJSONLogger(os.Stdout)
    logger = log.With(logger, "ts", log.DefaultTimestampUTC)
    logger = log.With(logger, "caller", log.DefaultCaller)

    addservice := service.NewService(logger)
    addendpoint := endpoints.MakeEndpoints(addservice)
    grpcServer := transport.NewGRPCServer(addendpoint, logger)

    errs := make(chan error)
    go func() {
        c := make(chan os.Signal)
        signal.Notify(c, syscall.SIGINT, syscall.SIGTERM, syscall.SIGALRM)
        errs <- fmt.Errorf("%s", <-c)
    }()

    grpcListener, err := net.Listen("tcp", ":50051")
    if err != nil {
        logger.Log("during", "Listen", "err", err)
        os.Exit(1)
    }

    go func() {
        baseServer := grpc.NewServer()
        pb.RegisterMathServiceServer(baseServer, grpcServer)
        level.Info(logger).Log("msg", "Server started successfully 🚀")
        baseServer.Serve(grpcListener)
    }()

    level.Error(logger).Log("exit", <-errs)
}
Enter fullscreen mode Exit fullscreen mode

Source Code

You can find the source code of this example at Github

Oldest comments (3)

Collapse
 
sedkis profile image
Sedky Abou-Shamalah

How does this compare to the API Gateway pattern, where all the non-business logic is handled at another layer?

Collapse
 
junereycasuga profile image
Junerey Casuga

I believe API Gateway pattern applies mostly as an infrastructure design for microservice rather than a code architecture.

IMO, Go kit's goal is to make the development of Go microservices easier/faster by providing essential packages/standard libraries rather than forcing someone on how they structure their code/infrasturcure

Collapse
 
xvbnm48 profile image
M Fariz Wisnu prananda

thanks for sharing