Construyendo microservicios con Golang y Go kit

Ellery Aguilar | 24 de noviembre, 2020

Apoyado por nuestro experto en innovación Ronald Hernández

La arquitectura de microservicios es un patrón de diseño que estructura las aplicaciones como una colección de servicios. En un mundo impulsado por la computación en la nube, los microservicios se han convertido en uno de los patrones más populares - por no decir el más popular - de diseño de arquitectura. Grandes compañías como Netflix y Amazon llevan años utilizándolos y cada día, más y más compañías hacen el esfuerzo de migrar de grandes arquitecturas monolíticas a los microservicios. Las siguientes son algunas de las ventajas de los microservicios:
  • Son fáciles de mantener y de poner a prueba.

  • Poseen bajo acoplamiento.

  • Se despliegan independientemente.

  • Se organizan con base a las capacidades del negocio.

  • Son propiedad de un grupo pequeño de personas.

Los microservicios son fáciles de mantener/probar, se escalan más eficientemente, utilizan menos recursos y se enfocan más en procesos específicos que brindan valor a la compañía. 

Pequeño y simple

Golang es un lenguaje de programación creado hace 10 años y diseñado por un equipo de Google. Sus características principales incluyen una sintaxis similar a C, es un lenguaje fuertemente tipado y cuenta con un recolector de basura. Además, ofrece un excelente manejo de concurrencias y programación paralela, por lo que funciona de maravilla con los microservicios.

“Al día de hoy las aplicaciones utilizan un número de servicios externos: bases de datos, caches, búsquedas y filas de mensajería. No obstante, más y más especialistas utilizan soluciones de microservicios debido a su colección de componentes separados. Al escribir código en Go, los desarrolladores utilizan entradas y salidas asincrónicas (I/O asincrónico) para que la aplicación pueda interactuar con un número de servicios sin bloquear solicitudes web.”

— Vyacheslav Pinchuk, desarrollador Golang de QArea

Otra característica relevante de Golang es que el núcleo de funcionalidades del lenguaje se mantiene pequeño. Este no es un lenguaje lleno de funcionalidades pre-construidas al que quizás estemos acostumbrados, como Python o Ruby. A cambio, se nos brinda una serie pequeña de capacidades básicas que podemos utilizar para crear capacidades más complejas. Pero, ¿esto significa que debemos escribir y preparar manualmente todo el soporte especializado para nuestro microservicio? Evidentemente, la respuesta es no. 

Go kit entra en escena

Go kit es un toolkit de programación para construir microservicios en Go. Fue creado para solucionar problemas comunes en sistemas distribuidos y aplicaciones y permitirle a los desarrolladores enfocarse en la parte comercial de la programación. Básicamente, es un grupo de paquetes relacionados que, en conjunto, crean un framework para construir grandes arquitecturas orientadas a servicios (SOA por sus siglas en inglés). Su principal capa de transporte es la llamada de procedimiento remoto (RPC por si siglas en inglés), pero también puede utilizar JSON por medio de HTTP y es fácil de integrar con los componentes infraestructurales más comunes, para reducir la fricción de despliegue y promover la interoperabilidad con sistemas existentes. 

Lógica de negocio

Vamos a crear un servicio mínimo Go kit con nuestra lógica de negocio. Siguiendo los ejemplos de Go kit, crearemos nuestro propio ‘string service’ que nos permita manipular los ‘strings’ en ciertas formas. Tomamos ventaja del tipo de interfaz Golang para definir nuestra lógica. Esto nos permite intercambiar implementaciones dependiendo de nuestras necesidades. 

 

 //File: service.go

    package mystr
    
    import (
    "errors"
    "strings"
    "github.com/go-kit/kit/log"
    )
    
    type Service interface {
    IsPal(string) error
    Reverse(string) string
    }
    
    type myStringService struct {
    log log.Logger
    }
    
    func (svc *myStringService) IsPal(s string) error {
    reverse := svc.Reverse(s)
    if strings.ToLower(s) != reverse {
      return errors.New("Not palindrome")
    }
    return nil
    }
    
    func (svc *myStringService) Reverse(s string) string {
    rns := []rune(s) // convert to rune
    for i, j := 0, len(rns)-1; i ‹ j; i, j = i+1, j-1 {
    
      // swap the letters of the string,
      // like first with last and so on.
      rns[i], rns[j] = rns[j], rns[i]
    }
    
    // return the reversed string.
    return strings.ToLower(string(rns))
    }

 

Solicitudes y respuestas

En Go kit, el principal patrón de mensajería es RPC. Esto significa que cada método en nuestra interfaz debe modelarse como una interacción client-server. El programa de solicitudes es un cliente, y el servicio es el servidor. Esto nos permite especificar los parámetros y los tipos de retorno para cada método. 

 


//requests.go

package mystr

type IsPalRequest struct {
Word string `json:"word"`
}

type ReverseRequest struct {
Word string `json:"word"`
}

 


//responses.go

package mystr

type IsPalResponse struct {
Message string `json:"message"`
}

type ReverseResponse struct {
Word string `json:"reversed_word"`
}

 

Endpoints

Aquí es donde entra en juego una de las principales funcionalidades de Go kit: ‘endpoints’. Estas son abstracciones que brinda Go kit y funcionan muy parecido a una acción o un ‘handler’ en un controlador. Es también donde se agrega la lógica de seguridad. Cada ‘endpoint’ representa un método único en la interfaz de nuestro servicio.

 


package mystr

import (
"context"
"errors"

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

type Endpoints struct {
GetIsPalindrome endpoint.Endpoint
GetReverse      endpoint.Endpoint
}

func MakeEndpoints(svc Service, logger log.Logger, middlewares []endpoint.Middleware) Endpoints {
return Endpoints{
  GetIsPalindrome: wrapEndpoint(makeGetIsPalindromeEndpoint(svc, logger), middlewares),
  GetReverse:      wrapEndpoint(makeGetReverseEndpoint(svc, logger), middlewares),
}
}

func makeGetIsPalindromeEndpoint(svc Service, logger log.Logger) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
  req, ok := request.(*IsPalRequest)
  if !ok {
    level.Error(logger).Log("message", "invalid request")
    return nil, errors.New("invalid request")
  }
  msg := svc.IsPal(ctx, req.Word)
  return &IsPalResponse{
    Message: msg,
  }, nil
}

}

func makeGetReverseEndpoint(svc Service, logger log.Logger) endpoint.Endpoint {
return func(ctx context.Context, request interface{}) (interface{}, error) {
  req, ok := request.(*ReverseRequest)
  if !ok {
    level.Error(logger).Log("message", "invalid request")
    return nil, errors.New("invalid request")
  }
  reverseString := svc.IsPal(ctx, req.Word)
  return &ReverseResponse{
    Word: reverseString,
  }, nil
}
}

func wrapEndpoint(e endpoint.Endpoint, middlewares []endpoint.Middleware) endpoint.Endpoint {
for _, m := range middlewares {
  e = m(e)
}
return e
}

 

Los ‘middlewares’ son una de las estructuras que brinda Go kit para garantizar la seguridad. Éstos funcionan del mismo modo en que lo hacen en otros lenguajes. Son métodos que se ejecutan por medio de la solicitud antes de que llegue a un ‘handler’. Aquí es donde se pueden añadir funcionalidades como ‘logging’, balance de carga, rastreo, etc. 

Registro

En el fragmento de código anterior introducimos dos importantes componentes de Go kit. El primero es, naturalmente, el paquete ‘endpoint’. El otro es el ‘logger’. Todos estamos familiarizados con los ‘logs’ (esa herramienta tan práctica que nos permite encontrar dónde suceden esos errores tan molestos). Go kit viene con su propio paquete ‘logger’, el cual nos permite escribir mensajes estructurados que son fáciles de consumir por parte de otros humanos u otras computadoras. Estos implementan una estructura de valores clave que nos permite crear un registro conjunto de varias piezas de información sin tener que escribir varios logs. 

 

Similarmente, también podemos utilizar el paquete ‘log level gokit’. Este paquete nos brinda una capa adicional de información. El tipo de log usual de error, debug, info, etc. al que estamos acostumbrados. 

Transporte

El archivo de transporte está destinado a representar la capa correspondiente del modelo OSI. Es el archivo que contiene todas las partes del código responsables por la estrategia de comunicación de principio a fin. Salido de fábrica, Go kit brinda soporte para muchos transportes. Para simplificar, trabajaremos con JSON por medio de HTTP.

 

El transporte consiste en una serie de objetos de Servidor, uno por cada ‘endpoint’ de nuestro servicio, y recibe cuatro parámetros. Estos son los siguientes:

 

  • Un ‘endpoint’: El ‘handler’ de la solicitud. Dada la separación de las estructuras y responsabilidades, podemos utilizar la misma estructura a lo largo de múltiples implementaciones de transporte. 
  • Una función de decodificación: Esta función recibe las solicitudes externas y las traduce a código Golang.
  • Una función de codificación: Es el opuesto de la función de decodificación. Esta función traduce la respuesta Golang a la salida correspondiente para el transporte seleccionado. 
  • Una serie de opciones de servidor: Esta serie de opciones podría ser credenciales, codec, mantener parámetros vivos, etc. Estos le brindan capacidades adicionales a nuestra capa de transporte. 

 


package mystr

import (
"context"
"encoding/json"
"errors"
"net/http"

"github.com/go-kit/kit/endpoint"
httptransport "github.com/go-kit/kit/transport/http"
)

func GetIsPalHandler(ep endpoint.Endpoint, options []httptransport.ServerOption) *httptransport.Server {
return httptransport.NewServer(
  ep,
  decodeGetIsPalRequest,
  encodeGetIsPalResponse,
  options...,
)
}

func decodeGetIsPalRequest(_ context.Context, r *http.Request) (interface{}, error) {
var req IsPalRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  return nil, err
}
return req, nil
}

func encodeGetIsPalResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
resp, ok := response.(*IsPalResponse)
if !ok {
  return errors.New("error decoding")
}
return json.NewEncoder(w).Encode(resp)
}

func GetReverseHandler(ep endpoint.Endpoint, options []httptransport.ServerOption) *httptransport.Server {
return httptransport.NewServer(
  ep,
  decodeGetReverseRequest,
  encodeGetReverseResponse,
  options...,
)
}

func decodeGetReverseRequest(_ context.Context, r *http.Request) (interface{}, error) {
var req ReverseRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  return nil, err
}
return req, nil
}

func encodeGetReverseResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
resp, ok := response.(*ReverseResponse)
if !ok {
  return errors.New("error decoding")
}

return json.NewEncoder(w).Encode(resp)
}

Atando cabos

Ahora ponemos a prueba nuestro trabajo. Tenemos todo lo necesario para que nuestro microservicio comience a trabajar. Ahora solamente debemos comenzar la escucha de solicitudes. 

 


package main

import (
"net/http"
"os"

"bitbucket.org/aveaguilar/stringsvc1/pkg/mystr"
"github.com/go-kit/kit/endpoint"
kitlog "github.com/go-kit/kit/log"
"github.com/go-kit/kit/log/level"
httptransport "github.com/go-kit/kit/transport/http"
"github.com/gorilla/mux"
)

func main() {
var logger kitlog.Logger
{
  logger = kitlog.NewLogfmtLogger(os.Stderr)
  logger = kitlog.With(logger, "ts", kitlog.DefaultTimestampUTC)
  logger = kitlog.With(logger, "caller", kitlog.DefaultCaller)
}

var middlewares []endpoint.Middleware
var options []httptransport.ServerOption
svc := mystr.NewService(logger)
eps := mystr.MakeEndpoints(svc, logger, middlewares)
r := mux.NewRouter()
r.Methods(http.MethodGet).Path("/palindrome").Handler(mystr.GetIsPalHandler(eps.GetIsPalindrome, options))
r.Methods(http.MethodGet).Path("/reverse").Handler(mystr.GetReverseHandler(eps.GetReverse, options))
level.Info(logger).Log("status", "listening", "port", "8080")
svr := http.Server{
  Addr:    "127.0.0.1:8080",
  Handler: r,
}
level.Error(logger).Log(svr.ListenAndServe())
}

 

Aquí, solamente estamos utilizando el paquete Golang HTTP básico para comenzar un servidor HTTP que escucha el puerto 8080. Utilizamos el paquete mux router para obtener un manejo más fácil del método HTTP. Cualquier error será capturado por el logger Go kit que hemos inicializado. 

Hay que recordar que debido a que estamos creando un servicio muy simple, no utilizamos ‘middlewares’ u opciones de servidor. Pero estas están disponibles para brindar funcionalidad y seguridad a nuestra aplicación. 

Así que ponemos en marcha nuestro servicio:


$go run cmd/stringsvc1/main.go
level=info ts=2020-07-26T00:44:15.447990239Z caller=main.go:30 status=listening port=8080

 

¡Y comenzamos con las solicitudes!


$curl -XGET '127.0.0.1:8080/palindrome' -d'{"word": "palindrome"}'
{"message":"Is not palindrome"}

 


$curl -X GET '127.0.0.1:8080/palindrome' -d'{"word": "1234554321"}'
{"message":"Is palindrome"}

 


$curl -X GET '127.0.0.1:8080/reverse' -d'{"word": "microservice"}'
{"reversed_word":"ecivresorcim"}

 

¡Y eso es todo! Aunque omitimos algunos de los paquetes básicos, este es un ejemplo de lo mínimo que necesitamos hacer para poder crear nuestro propio servicio. Solo con un poco de archivos y líneas de código, podemos crear una aplicación. Go kit hace que sea muy fácil. 

Es clave visitar el sitio web de Go kit para más información. Brinda ejemplos más complejos así como muy buenos artículos que permiten conocer el kit más a fondo. Además, GoDoc es un excelente recurso con el que se puede contar al adentrarse en el mundo de Golang.

 

Referencias

 

 

idea

Puntos Clave

  1. La arquitectura de microservicios es un patrón de diseño que estructura las aplicaciones como una colección de servicios y se ha convertido en uno de los patrones de diseño de arquitectura más populares.
  2. Golang es un lenguaje de programación con una sintaxis similar a C, es un lenguaje fuertemente tipado y cuenta con un recolector de basura; además, ofrece un excelente manejo de concurrencias y programación paralela, por lo que funciona de maravilla con los microservicios.
  3. Go kit, un toolkit de programación para construir microservicios en Go, fue creado para solucionar problemas comunes en sistemas distribuidos y aplicaciones y permitirle a los desarrolladores enfocarse en la parte comercial de la programación.

 

Contáctenos

Contenido

Compartir Artículo

Artículos Destacados