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

от автора

Салют, Хабр!

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.


ссылка на оригинал статьи https://habr.com/ru/articles/793888/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *