golang

Обзор библиотеки Go Kit

  • вторник, 20 февраля 2024 г. в 00:00:13
https://habr.com/ru/companies/otus/articles/793888/

Салют, Хабр!

Go Kit предоставляет стандартизированный способ создания сервисов, с ее помощью можно легко реализовать совместимость сервисов. С его помощью можно легко интегрировать различные транспортные протоколы, такие как HTTP, RPC, gRPC, и многое другое, а также реализовывать общие паттерны: логирование, метрики, трассировка. В общем, Go Kit хорошо подходит для разработки микросервисов на go.

Мотивацию создания этой либы разработчики описали так:

Go стал языком сервера, но он по-прежнему недостаточно представлен в так называемых «современных корпоративных» компаниях, таких как Facebook, Twitter, Netflix и SoundCloud. Многие из этих организаций обратились к стекам на основе JVM для создания своей бизнес-логики, во многом благодаря библиотекам и экосистемам, которые напрямую поддерживают их микросервисные архитектуры.

Чтобы достичь следующего уровня успеха, Go нужно нечто большее, чем простые примитивы и идиомы. Ему нужен всеобъемлющий набор инструментов для последовательного распределенного программирования в целом. Go Kit — это набор пакетов и лучших практик, которые обеспечивают комплексный, надежный и надежный способ создания микросервисов для организаций любого размера.

Также стоит упомянуть, что разработчики не ставят цель реализовать следующие функции:

  • Поддержка шаблонов обмена сообщениями, отличных от RPC (на данный момент), например MPI, pub/sub, CQRS и т. д.

  • Повторная реализация функциональности, которую можно обеспечить путем адаптации существующего программного обеспечения.

Установка Go Kit:

go get -u github.com/go-kit/kit

Go Kit требует Go версии 1.13 или выше.

Компоненты Go Kit

Сервисы — это база микросервисной архитектуры. Каждый сервис представляет собой отдельный компонент, который выполняет определенную функцию или набор функций. В Go Kit сервисы разрабатываются как наборы интерфейсов и реализаций, которые разделяют бизнес-логику от остальной части системы.

Транспортный слой является мостом между вашими сервисами и внешним миром. Он отвечает за прием запросов от клиентов, их обработку и передачу данных обратно клиентам. Go Kit предлагает систему транспортных слоев, поддерживающих множество протоколов, включая HTTP, gRPC, Thrift и т.д.

Endpoints представляют собой конечные точки, к которым обращаются клиенты для выполнения определенных операций. Endpoints отвечают за обработку входящих запросов, выполнение соответствующих вызовов сервисов и возврат результатов обратно клиентам.

Основные функции

Сервисный слой

Сервисный слой начинается с определения интерфейса. Интерфейс сервиса описывает операции или действия, которые можно выполнить в рамках данного сервиса. Это абстракция, которая скрывает детали реализации, позволяя фокусироваться на том, что делает сервис, а не как он это делает.

Допустим нужен микросервис для управления пользователями. На уровне интерфейса это может выглядеть так:

package userservice

// userService определяет интерфейс для нашего сервиса управления пользователями.
type UserService interface {
    CreateUser(name string, email string) (User, error)
    GetUser(id string) (User, error)
}

UserService предоставляет две операции: CreateUser для создания нового пользователя и GetUser для получения информации о пользователе по его идентификатору. Возвращаемые значения и ошибки указывают на результат выполнения каждой операции.

После определения интерфейса следующим шагом будет реализация этого интерфейса. Реализация — это конкретный код, который выполняет логику, описанную интерфейсом:

package userservice

import "errors"

// userService представляет реализацию нашего UserService.
type userService struct {
    // здесь различные зависимости, ссылки на бд и т.п
}

// NewUserService создает и возвращает новый экземпляр userService.
func NewUserService() UserService {
    return &userService{}
}

// CreateUser реализует логику создания пользователя.
func (s *userService) CreateUser(name string, email string) (User, error) {
    // логика создания нового пользователя.
    // проверка валидности данных и запись пользователя в базу данных
    return User{Name: name, Email: email}, nil
}

// GetUser реализует логику получения пользователя по ID.
func (s *userService) GetUser(id string) (User, error) {
    // логика поиска пользователя по его ID в базе данных.
    // если пользователь не найден, возвращается ошибка.
    return User{}, errors.New("user not found")
}

// ser представляет модель пользователя в нашей системе.
type User struct {
    ID    string
    Name  string
    Email string
}

userService является приватной структурой, которая реализует интерфейс UserService. ФункцияNewUserServiceнужно чтобы скрыть детали создания экземпляра сервиса и возвращаем интерфейс, а не конкретный тип.

Endpoint слой

Допустим, у нас есть сервис UserService с методом GetUser, который мы хотим экспонировать через HTTP. Сначала определим endpoint:

import (
    "context"
    "github.com/go-kit/kit/endpoint"
)

// GetUserRequest определяет структуру запроса к endpoint.
type GetUserRequest struct {
    UserID string
}

// GetUserResponse определяет структуру ответа от endpoint.
type GetUserResponse struct {
    User  User   `json:"user,omitempty"`
    Err   string `json:"err,omitempty"` // ошибки не сериализуются по JSON напрямую.
}

// MakeGetUserEndpoint создает endpoint для метода GetUser сервиса UserService.
func MakeGetUserEndpoint(svc UserService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(GetUserRequest)
        user, err := svc.GetUser(req.UserID)
        if err != nil {
            return GetUserResponse{User: user, Err: err.Error()}, nil
        }
        return GetUserResponse{User: user, Err: ""}, nil
    }
}

Крейтнули endpoint, который принимает запрос GetUserRequest, извлекает UserID и использует его для вызова метода GetUser нашего сервиса UserService. Ответ от сервиса затем оборачивается в GetUserResponse.

Middleware позволяет добавлять перехватывающую логику в обработку запросов, например, для логирования, мониторинга, проверки аутентификации и т.д., не изменяя логику самих endpoints.

Проще говоря, middleware представляет собой функцию, которая принимает endpoint и возвращает другой endpoint:

import (
    "context"
    "github.com/go-kit/kit/endpoint"
    "github.com/go-kit/kit/log"
)

// LoggingMiddleware возвращает Middleware, которое логирует запросы к сервису.
func LoggingMiddleware(logger log.Logger) endpoint.Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (response interface{}, err error) {
            logger.Log("msg", "calling endpoint")
            response, err = next(ctx, request)
            logger.Log("msg", "called endpoint")
            return
        }
    }
}

Здесь middleware логирует сообщения до и после вызова оригинального endpoint. Можно применить это middleware к любому endpoint сервиса, передав его через MakeGetUserEndpoint, например, или к любому другому endpoint.

Endpoints можно группировать. Группировка endpoints достигается через создание набора endpoints, который представляет собой агрегацию всех endpoints, связанных с определенным сервисом.

Определим несколько базовых endpoints для нашего примера сервиса, который будем группировать. К примеру есть ProfileService, предоставляющий функционал для управления профилями пользователей:

type ProfileService interface {
    CreateProfile(ctx context.Context, profile Profile) (string, error)
    GetProfile(ctx context.Context, id string) (Profile, error)
}

Для каждого метода интерфейса сервиса определим соответствующий endpoint.

func makeCreateProfileEndpoint(svc ProfileService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(createProfileRequest)
        id, err := svc.CreateProfile(ctx, req.Profile)
        return createProfileResponse{ID: id, Err: err}, nil
    }
}

func makeGetProfileEndpoint(svc ProfileService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(getProfileRequest)
        profile, err := svc.GetProfile(ctx, req.ID)
        return getProfileResponse{Profile: profile, Err: err}, nil
    }
}

Теперь есть несколько endpoints, их можно группировать вместе. Это обычно делается путем создания структуры, которая содержит все эти endpoints как поля:

type Endpoints struct {
    CreateProfile endpoint.Endpoint
    GetProfile    endpoint.Endpoint
}

func MakeEndpoints(svc ProfileService) Endpoints {
    return Endpoints{
        CreateProfile: makeCreateProfileEndpoint(svc),
        GetProfile:    makeGetProfileEndpoint(svc),
    }
}

Структура Endpoints теперь агрегирует все endpoints, связанные с ProfileService, вроде - удобно.

После группировки endpoints их можно использовать в транспортном слое (про транспортные слое чуть ниже). Например, при создании HTTP сервера, можно ссылаться на эти endpoints напрямую из структуры Endpoints:

func NewHTTPHandler(endpoints Endpoints) http.Handler {
    r := mux.NewRouter()

    r.Methods("POST").Path("/profiles").Handler(httptransport.NewServer(
        endpoints.CreateProfile,
        decodeHTTPCreateProfileRequest,
        encodeHTTPGenericResponse,
    ))

    r.Methods("GET").Path("/profiles/{id}").Handler(httptransport.NewServer(
        endpoints.GetProfile,
        decodeHTTPGetProfileRequest,
        encodeHTTPGenericResponse,
    ))

    return r
}

Транспортный слой

Для создания HTTP сервера определяются транспортные функции, которые преобразуют HTTP запросы в вызовы вашего сервиса и ответы сервиса обратно в HTTP ответы:

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "github.com/go-kit/kit/endpoint"
    "github.com/go-kit/kit/transport/http"
)

// сервис
type MyService interface {
    Add(a, b int) int
}

type myService struct{}

func (myService) Add(a, b int) int { return a + b }

// endpoint
func makeAddEndpoint(svc MyService) endpoint.Endpoint {
    return func(ctx context.Context, request interface{}) (interface{}, error) {
        req := request.(addRequest)
        v := svc.Add(req.A, req.B)
        return addResponse{V: v}, nil
    }
}

type addRequest struct {
    A int `json:"a"`
    B int `json:"b"`
}

type addResponse struct {
    V int `json:"v"`
}

// decode и encode функции
func decodeAddRequest(_ context.Context, r *http.Request) (interface{}, error) {
    var request addRequest
    if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
        return nil, err
    }
    return request, nil
}

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
    return json.NewEncoder(w).Encode(response)
}

func main() {
    svc := myService{}

    addEndpoint := makeAddEndpoint(svc)
    addHandler := http.NewServer(addEndpoint, decodeAddRequest, encodeResponse)

    http.Handle("/add", addHandler)
    http.ListenAndServe(":8080", nil)
}

Создаем простой сервис MyService с методом Add, который складывает два числа. Затем создается endpoint, который обрабатывает логику преобразования запросов и ответов. Для обработки HTTP запросов и ответов используем http.NewServer.

Для создания HTTP клиента используется аналогичная абстракция:

package main

import (
    "context"
    "encoding/json"
    "net/http"
    "github.com/go-kit/kit/endpoint"
    "github.com/go-kit/kit/transport/http"
)

func makeHTTPClient(baseURL string) MyService {
    var addEndpoint endpoint.Endpoint
    addEndpoint = http.NewClient(
        "POST",
        mustParseURL(baseURL+"/add"),
        encodeHTTPRequest,
        decodeHTTPResponse,
    ).Endpoint()

    return Endpoints{AddEndpoint: addEndpoint}
}

func encodeHTTPRequest(_ context.Context, r *http.Request, request interface{}) error {
    // код для кодирования запроса
}

func decodeHTTPResponse(_ context.Context, resp *http.Response) (interface{}, error) {
    var response addResponse
    if err := json.NewDecoder(resp.Body).Decode(&response); err != nil {
        return nil, err
    }
    return response, nil
}

Go Kit предлагает встроенную поддержку для gRPC:

Определим интерфейс сервиса и структуры данных, которые он использует, в .proto файле. fe:

syntax = "proto3";

package example;

service StringService {
  rpc Uppercase (UppercaseRequest) returns (UppercaseResponse) {}
  rpc Count (CountRequest) returns (CountResponse) {}
}

message UppercaseRequest {
  string str = 1;
}

message UppercaseResponse {
  string str = 1;
  string err = 2;
}

message CountRequest {
  string str = 1;
}

message CountResponse {
  int32 count = 1;
}

Используя protoc компилятор с плагином для Go, можно сгенерировать Go код, который будет использоваться для создания gRPC сервера:

protoc --go_out=. --go-grpc_out=. path/to/your_service.proto

Далее реализуем сервис в Go, используя интерфейсы, сгенерированные из .proto файла:

package main

import (
    "context"
    "strings"
    "google.golang.org/grpc/codes"
    "google.golang.org/grpc/status"
    pb "path/to/your_service_package"
)

type stringService struct {
    pb.UnimplementedStringServiceServer
}

func (s *stringService) Uppercase(ctx context.Context, req *pb.UppercaseRequest) (*pb.UppercaseResponse, error) {
    if req.Str == "" {
        return nil, status.Errorf(codes.InvalidArgument, "Empty string")
    }
    return &pb.UppercaseResponse{Str: strings.ToUpper(req.Str)}, nil
}

func (s *stringService) Count(ctx context.Context, req *pb.CountRequest) (*pb.CountResponse, error) {
    return &pb.CountResponse{Count: int32(len(req.Str))}, nil
}

run:

package main

import (
    "log"
    "net"
    "google.golang.org/grpc"
    pb "path/to/your_service_package"
)

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    var opts []grpc.ServerOption
    grpcServer := grpc.NewServer(opts...)
    pb.RegisterStringServiceServer(grpcServer, newStringService())
    grpcServer.Serve(lis)
}

Здесь так же как и в эндпоинтах есть Middleware, который в транспортном слое позволяет встраивать дополнительную логику обрабтки для входящих и исходящих запросов/ответов:

package main

import (
    "context"
    "fmt"
    "github.com/go-kit/kit/endpoint"
    "github.com/go-kit/log"
)

// LoggingMiddleware возвращает Middleware, которое логирует детали запроса
func LoggingMiddleware(logger log.Logger) endpoint.Middleware {
    return func(next endpoint.Endpoint) endpoint.Endpoint {
        return func(ctx context.Context, request interface{}) (response interface{}, err error) {
            logger.Log("msg", "calling endpoint")
            defer func() {
                logger.Log("msg", "called endpoint", "err", err)
            }()
            return next(ctx, request)
        }
    }
}

Прочие возможности

В го кит есть абстракция логирования, которая позволяет легко интегрировать любую систему логирования с вашими сервисами. Например, мой любимыйlog.Logger, который отличается своей минималистичностью:

import (
    "github.com/go-kit/log"
    "github.com/sirupsen/logrus"
)

type logrusLogger struct {
    *logrus.Logger
}

func (l logrusLogger) Log(keyvals ...interface{}) error {
    // здесь может быть реализация адаптация аргументов keyvals
    // для logrus или другой логики адаптации.
    l.Logger.WithFields(logrus.Fields{"keyvals": keyvals}).Info()
    return nil
}

// экземпляр Logger Go Kit, используя logrus
logger := logrusLogger{logrus.New()}

Можно интегрироваться с системами метрик, к примеру с Prometheus:

import (
    "github.com/go-kit/kit/metrics/prometheus"
    stdprometheus "github.com/prometheus/client_golang/prometheus"
)

var requestCount = prometheus.NewCounterFrom(stdprometheus.CounterOpts{
    Namespace: "my_namespace",
    Subsystem: "my_subsystem",
    Name:      "request_count",
    Help:      "Number of requests received.",
}, []string{"method"})

Можно интегрироваться с системами трассировки, к примеру Jaeger:

import (
    "github.com/go-kit/kit/tracing/opentracing"
    "github.com/opentracing/opentracing-go"
    "github.com/uber/jaeger-client-go/config"
)

// сеттинги Jaeger
cfg, _ := config.FromEnv()
tracer, _, _ := cfg.NewTracer()

// трассировка в endpoint
tracedEndpoint := opentracing.TraceServer(tracer, "my_endpoint")(myEndpoint)

В примерах ранее уже реализовывали обработку ошибок, но думаю, в этом разделе стоит эту функцию включить, к примеру обработку польз.ошибки:

import (
    "errors"
    "net/http"
    "github.com/go-kit/kit/transport/http"
)

var ErrInvalidArgument = errors.New("invalid argument")

// прнбразование ошибки в HTTP статус
errorEncoder := func(ctx context.Context, err error, w http.ResponseWriter) {
    code :=

 http.StatusInternalServerError
    if err == ErrInvalidArgument {
        code = http.StatusBadRequest
    }
    w.WriteHeader(code)
    json.NewEncoder(w).Encode(map[string]interface{}{
        "error": err.Error(),
    })
}

Таким образом с go kit можно решить типичные проблемы в распределенных системах и архитектуре приложений. Go Kit на гитхабе, сайт Go Kit


Статья подготовлена в преддверии старта курса "Microservice Architecture" от OTUS.