Как стать автором
Обновить
1157.54
OTUS
Цифровые навыки от ведущих экспертов

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

Уровень сложностиПростой
Время на прочтение10 мин
Количество просмотров4.6K

Салют, Хабр!

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.

Теги:
Хабы:
Всего голосов 10: ↑6 и ↓4+4
Комментарии2

Публикации

Информация

Сайт
otus.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия
Представитель
OTUS