Обзор библиотеки Go Kit
- вторник, 20 февраля 2024 г. в 00:00:13
Салют, Хабр!
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 предлагает систему транспортных слоев, поддерживающих множество протоколов, включая 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
нужно чтобы скрыть детали создания экземпляра сервиса и возвращаем интерфейс, а не конкретный тип.
Допустим, у нас есть сервис 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.