Перейти к содержимому
ГоМастер

Поиск по урокам

Найдите урок, курс, термин или раздел

Урок 13 из 25 ~22 мин +50 XP

gRPC транспорт в go-kit

Фабрика из урока 12 строит HTTP-ноги, но индустрия для service-to-service выбирает gRPC: бинарный protobuf поверх HTTP/2 и контракт в .proto, из которого кодогенерация выдаёт структуры и интерфейс сервиса. Готовый go kit grpc-адаптер живёт в kit/transport/grpc — это тот же grpc transport, что и HTTP: на входе endpoint, по бокам те же кодеки, только вместо *http.Request каркас gRPC передаёт уже разобранные proto-сообщения. Ты соберёшь proto-контракт stringsvc, свяжешь endpoint'ы из урока 02 с grpctrans.NewServer через ServeGRPC, поднимешь второй слушатель на :8082 рядом с HTTP-сервером из урока 04, пропишешь metadata и интерсепторы каркаса, а на выходе — gRPC-клиент, который встаёт в sd.Factory из урока 12 и закрывает conn вместе с вычеркнутым инстансом.

Содержание урока · 14 разделов

HTTP уже есть: зачем второй транспорт#

Сервис из уроков 04-10 отдаёт миру JSON по HTTP — и для публичного API это правильный выбор: читаемо, отлаживается curl'ом, открывается любым клиентом. Но внутри кластера сервисы зовут сервисы десятки раз на каждый внешний запрос, и там HTTP/1.1 с JSON начинает мешать: текстовые заголовки на каждый вызов, отсутствие контракта в коде — схема запроса живёт в догадках и чужих README, а клиентские библиотеки каждый пишет сам. gRPC решает обе проблемы: контракт объявляется в .proto-файле, из него кодогенерация выдаёт структуры и клиентский код на любом языке, а перевозкой занимается бинарный protobuf поверх HTTP/2 — мультиплексирование многих вызовов на одном TCP-соединении, без Head-of-Line блокировки HTTP/1.1. Позиция go-kit остаётся той же, что в уроке 01-filosofiya: транспорт — плагин, и gRPC — просто ещё один плагин рядом с HTTP. Endpoint не меняется, обёртки из уроков 06-10 не меняются, сервис не знает, кто его вызвал. Это и есть обещание «go kit grpc — то же ядро, другие ноги»: готовый адаптер живёт в пакете kit/transport/grpc, и сейчас мы его смонтируем. Когда gRPC не нужен: браузер (для него есть отдельная история grpc-web), публичный API и интеграции, где проще отдал JSON-форму, чем объяснять партнёру protoc.

Proto-контракт stringsvc#

Контракт начинается с файла. proto3 описывает service Strings с двумя rpc — Uppercase и Count, ровно те же операции, что в интерфейсе сервиса из урока 03, — и message-типы, зеркальные request/response-структурам из урока 02: поле s на входе, поля v и err на выходе, c для счётчика. Номера после знака равенства — это proto-аналог json-тегов из урока 02: уникальный идентификатор поля в бинарном формате, и менять их после релиза нельзя, как нельзя менять json-теги. Err-поле объявлено строкой — сериализуемой, в отличие от error; бизнес-ошибка едет в ответе, как ты привык. Компилятор protoc с двумя плагинами выдаёт pb-пакет: структуры UppercaseRequest и UppercaseReply, интерфейс StringsServer, который должен реализовать твой сервер, глобальную переменную Strings_ServiceDesc — описание сервиса для каркаса — и функцию RegisterStringsServer, связывающую каркас с твоей реализацией. Обрати внимание на правило из урока 05 в новом свете: кодеки — единственное место, где легальны транспортные типы. Здесь транспортными типами стали сгенерированные структуры pb.*, и граница не сдвинулась: endpoint и сервис про protobuf не узнают никогда.

stringsvc.proto и кодогенерация

GO
syntax = "proto3";

package strings;
option go_package = "example.com/stringsvc/pb";

service Strings {
        rpc Uppercase (UppercaseRequest) returns (UppercaseReply) {}
        rpc Count (CountRequest) returns (CountReply) {}
}

message UppercaseRequest { string s = 1; }

message UppercaseReply {
        string v   = 1;
        string err = 2;
}

message CountRequest { string s = 1; }
message CountReply { int64 c = 1; }

package strings задаёт имя сервиса целиком: strings.Strings — по нему каркас находит обработчик, и полный путь метода выглядит как /strings.Strings/Uppercase. option go_package говорит генератору, в какой пакет положить код — pb относительно корня модуля. Команда генерации: protoc -I . --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative stringsvc.proto — плагин protoc-gen-go выдаёт структуры и сериализацию, protoc-gen-go-grpc — интерфейс StringsServer, Strings_ServiceDesc и RegisterStringsServer. Поля v и err выровнены для читаемости, номера 1 и 2 — контракт бинарной раскладки: переименовать поле можно, перенумеровать — нельзя.

grpctrans.NewServer: Handler, а не transport.Server#

Здесь gRPC-транспорт сюрпризит того, кто привык к HTTP. kithttp.NewServer возвращает http.Handler: ты монтируешь его на mux, и каркас net/http сам зовёт ServeHTTP — go-kit контролирует всю дорогу от запроса до endpoint'а. У gRPC стандартная точка входа другая: каркас google.golang.org/grpc вызывает методы ТВОЕЙ реализации сгенерированного интерфейса StringsServer — и вставиться в эту цепочку извне библиотека не может. Поэтому grpctrans.NewServer(ep, dec, enc, options...) возвращает не готовый http.Handler, а grpctrans.Handler — объект с одним методом ServeGRPC(ctx, request) (context.Context, interface{}, error): принимает уже разобранное каркасом proto-сообщение, зовёт endpoint через декодер и энкодер, возвращает сгенерированный ответ. Склейкой занимается тонкий адаптер — твой grpcServer: каждый метод интерфейса делает три строки — передать запрос в ServeGRPC и type-assert ответа. Кодеки при этом знакомые до боли: те же имена типов DecodeRequestFunc и EncodeResponseFunc, что и в kithttp, сигнатура func(context.Context, interface{}) (interface{}, error) — только вместо http.Request на вход приходит pb.UppercaseRequest, и декодеру почти нечего парсить: каркас уже превратил байты в структуру.

gRPC-кодеки: сгенерированное в доменное и обратно

GO
// Типы UppercaseRequest / UppercaseResponse — из урока 02,
// структуры pb.* — из кодогенерации выше.
func decodeGRPCUppercaseRequest(_ context.Context, grpcReq interface{}) (interface{}, error) {
        req := grpcReq.(*pb.UppercaseRequest)
        return UppercaseRequest{S: req.S}, nil
}

func encodeGRPCUppercaseResponse(_ context.Context, response interface{}) (interface{}, error) {
        resp := response.(UppercaseResponse)
        return &pb.UppercaseReply{V: resp.V, Err: resp.Err}, nil
}

Два анти-коррупционных шва по четыре строки — и весь protobuf остался внутри них. Декодер перекладывает сгенерированную структуру в доменную пару урока 02: type-assert — единственное место, где мы верим каркасу, что пришёл именно pb.UppercaseRequest. Энкодер зеркален: Err-поле едет строкой в UppercaseReply — бизнес-ответ сохраняет свой канал, Failer из урока 02 продолжает работать. Сериализацию делает сам каркас (protobuf), поэтому кодеки не трогают байты вовсе — в HTTP-энкодере была json.Marshal, здесь её нет. Пара Count-кодеков пишется по той же схеме: CountRequest → CountRequest, CountResponse → pb.CountReply.

Биндинг: grpcServer над сгенерированным интерфейсом#

Адаптер — обычная структура с двумя Handler'ами по числу rpc. Обязательная деталь: встраивание pb.UnimplementedStringsServer. Генератор требует, чтобы реализация встраивала этот тип (в интерфейсе есть mustEmbedUnimplementedStringsServer) — страховка совместимости: когда в .proto добавят новый rpc, старый сервер не упадёт при регистрации, а ответит честным codes.Unimplemented. Методы grpcServer повторяют сигнатуры интерфейса: ctx и pb.UppercaseRequest на входе, pb.UppercaseReply и error на выходе. Три строки тела: позвать ServeGRPC, вернуть ошибку как есть, привести ответ к нужному типу — тот самый шов из урока 02, теперь в обратную сторону. Конструктор NewGRPCServer принимает готовые endpoint'ы — те самые makeUppercaseEndpoint и makeCountEndpoint из урока 02, уже обёрнутые логами, метриками, спанами, лимитером и breaker'ом, — и на каждый строит свой grpctrans.Handler со своими кодеками. Никакого состояния, никаких goroutine: адаптер существует только чтобы каркас и endpoint встретились.

grpctrans.Handler в деле

GO
import (
        pb "example.com/stringsvc/pb"

        "github.com/go-kit/kit/endpoint"
        grpctrans "github.com/go-kit/kit/transport/grpc"
)

type grpcServer struct {
        pb.UnimplementedStringsServer
        uppercase grpctrans.Handler
        count     grpctrans.Handler
}

func (s *grpcServer) Uppercase(ctx context.Context, req *pb.UppercaseRequest) (*pb.UppercaseReply, error) {
        _, rep, err := s.uppercase.ServeGRPC(ctx, req)
        if err != nil {
                return nil, err
        }
        return rep.(*pb.UppercaseReply), nil
}

func (s *grpcServer) Count(ctx context.Context, req *pb.CountRequest) (*pb.CountReply, error) {
        _, rep, err := s.count.ServeGRPC(ctx, req)
        if err != nil {
                return nil, err
        }
        return rep.(*pb.CountReply), nil
}

func NewGRPCServer(uppercase, count endpoint.Endpoint) pb.StringsServer {
        return &grpcServer{
                uppercase: grpctrans.NewServer(
                        uppercase,
                        decodeGRPCUppercaseRequest,
                        encodeGRPCUppercaseResponse,
                ),
                count: grpctrans.NewServer(
                        count,
                        decodeGRPCCountRequest,
                        encodeGRPCCountResponse,
                ),
        }
}

Вся связка каркаса с ядром курса — один файл. Заметь: методы не делают ничего, кроме маршрутизации в свои Handler'ы, — если бы в go-kit не было gRPC-транспорта, тебе пришлось бы городить этот переезд руками в каждом методе, и обёртки из уроков 06-10 остались бы за бортом. Ошибки из err канала уезжают наверх как есть — каркас превратит их в gRPC-статус; как управлять кодами — через два блока. Опции вроде ServerBefore передаются третьим и дальше аргументом в каждый NewServer — о них ниже.

main: второй слушатель рядом с HTTP

GO
grpcListener, err := net.Listen("tcp", ":8082")
if err != nil {
        logger.Log("transport", "gRPC", "during", "Listen", "err", err)
        os.Exit(1)
}

baseServer := grpc.NewServer(
        grpc.ChainUnaryInterceptor(recoveryInterceptor, authInterceptor),
)
pb.RegisterStringsServer(baseServer, NewGRPCServer(uppercase, count))

logger.Log("transport", "gRPC", "addr", ":8082")
baseServer.Serve(grpcListener)

Тот же main, второй слушатель: HTTP из урока 04 продолжает жить на :8081, gRPC встаёт на :8082 — и оба едят ОДНИ и те же endpoint'ы: переменные uppercase и count собраны по канону уроков 06-10 и переданы в оба транспорта. Никакого дублирования логики — транспорт-агностичность из урока 01 на деле. grpc.NewServer — это каркас google.golang.org/grpc, RegisterStringsServer — сгенерированная связка каркаса с твоим grpcServer, ChainUnaryInterceptor — цепочка перехватчиков, о которых отдельный разговор ниже. Для остановки без обрыва живых вызовов у каркаса есть baseServer.GracefulStop() — дренаж соединений разберём в уроке про graceful shutdown. Ручной вызов вместо curl — grpcurl: grpcurl -plaintext -d '{"s":"hi"}' localhost:8082 strings.Strings/Uppercase.

Ошибки: два канала, как в HTTP#

Канон из уроков 02 и 05 в gRPC работает дословно, потому что он не про HTTP, а про разделение каналов. Бизнес-ошибка лежит в Err-поле response-структуры с Failer — и в gRPC-энкодере она едет строкой в *pb.UppercaseReply.Err: клиент получит валидный ответ с err="empty string" и err-канал не дёрнет. Инфраструктурный сбой — дедлайн, недоступная база, сломанный декодер — возвращается по err канала, и здесь вступает каркас: ошибки контекста он конвертирует сам, context.DeadlineExceeded станет кодом DeadlineExceeded, а любую незнакомую ошибку — кодом Unknown с текстом. Unknown — это «непонятно что»: такой код не отличит плохой запрос от аварии, алерты и ретраи по нему слепы. Лечится опцией grpctrans.ServerErrorEncoder — она принимает ErrorEncoder func(ctx, err) error, который возвращает status.Error с осмысленным кодом: прямой аналог твоей таблицы codeFrom из урока 04, только вместо HTTP-статусов — коды gRPC. Правило то же: sentinel-ошибки сервиса мапишь сознательно, всё остальное честно падает в Internal.

ServerErrorEncoder: таблица кодов

GO
grpctrans.NewServer(
        uppercase,
        decodeGRPCUppercaseRequest,
        encodeGRPCUppercaseResponse,
        grpctrans.ServerErrorEncoder(func(ctx context.Context, err error) error {
                switch {
                case errors.Is(err, context.DeadlineExceeded):
                        return status.Error(codes.DeadlineExceeded, "endpoint deadline")
                case errors.Is(err, ErrDBUnavailable): // sentinel твоего сервиса
                        return status.Error(codes.Unavailable, err.Error())
                default:
                        return status.Error(codes.Internal, err.Error())
                }
        }),
)

Таблица codeFrom в gRPC-одежде: errors.Is по sentinel-ошибкам, status.Error(codes.X, ...) на выходе. ErrEmpty здесь НЕ ветка — и это осознанно: пустая строка на входе по канону урока 02 — бизнес-ответ, она уехала в Err-поле reply и до error encoder'а не доходит. Если sentinel'ы сервиса начнут мапиться сюда, клиентская сторона увидит осмысленные коды: Unavailable — можно ретраить, как советовал lb.Retry из урока 11, InvalidArgument — повторять бессмысленно. Проверь себя: если ErrEmpty вдруг появится в этой таблице — значит, где-то выше он попал не в тот канал, и breaker'ы начнут размыкаться на валидных ответах.

Metadata: заголовки gRPC#

У HTTP-запроса были заголовки, у gRPC — metadata.MD: пары ключ-значение, где ключи в нижнем регистре, а каждое значение — срез строк, потому что один ключ может повторяться. Транспорт читает и пишет metadata теми же хуками, к которым ты привык в HTTP, только имена другие. ServerBefore принимает ServerRequestFunc — func(ctx, *metadata.MD) context.Context: функция достаёт из md.Get("x-request-id") значения и кладёт их в контекст — request_id из урока 06 переезжает на новый транспорт без единой правки в логах. Клиентская половина зеркальна: ClientBefore принимает ClientRequestFunc — из контекста в исходящий md через md.Set или md.Append. А для трейсинга go-kit даёт готовую пару: kitot.GRPCToContext(tracer, operationName, logger) на сервере и kitot.ContextToGRPC(tracer, logger) на клиенте — обещание из урока 08-tracing выполнено, SpanContext путешествует в metadata, как uber-trace-id в HTTP-заголовках. ServerAfter работает на выходе: его функции дописывают ответные заголовки — аналог финализатора. Вся наблюдаемость курса — логи 06, метрики 07, спаны 08 — продолжает работать, потому что она стоит на контексте, а контекст транспорт поставляет всегда.

ServerRequestFunc и ClientRequestFunc

GO
// Серверная половина: metadata входящего вызова → контекст.
func requestIDToContext() grpctrans.ServerRequestFunc {
        return func(ctx context.Context, md *metadata.MD) context.Context {
                if ids := md.Get("x-request-id"); len(ids) > 0 {
                        return context.WithValue(ctx, requestIDKey, ids[0])
                }
                return ctx
        }
}

// Клиентская половина: контекст → metadata исходящего вызова.
func requestIDToMetadata() grpctrans.ClientRequestFunc {
        return func(ctx context.Context, md *metadata.MD) context.Context {
                if id, ok := ctx.Value(requestIDKey).(string); ok {
                        md.Set("x-request-id", id)
                }
                return ctx
        }
}

requestIDKey — тот же закрытый тип-ключ из урока 06: контекст остаётся единственным шиной наблюдаемости. md.Get возвращает срез — «есть ли значения» проверяется длиной; md.Set перезаписывает, md.Append добавляет к существующим. Подключение на сервере: grpctrans.ServerBefore(requestIDToContext(), kitot.GRPCToContext(tracer, "gRPC /strings.Strings/Uppercase", logger)) — порядок как в HTTP: сначала свои ключи, потом спан; имя операции по конвенции урока 08 — «транспорт + маршрут». На клиенте зеркально: grpctrans.ClientBefore(requestIDToMetadata(), kitot.ContextToGRPC(tracer, logger)) — request_id и спан уезжают к соседу, Jaeger склеит сервисы в одно дерево. Ключи metadata каркас нормализует в нижний регистр — пиши сразу так, иначе удивишься пустому md.Get.

Интерсепторы каркаса против ServerBefore#

У gRPC есть собственный перехватный механизм, и важно не путать его с опциями go-kit. Интерсептор — функция func(ctx, req, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error): она обёрнута вокруг ВСЕГО вызова метода, от каркаса до ответа; в info.FullMethod лежит путь вида /strings.Strings/Uppercase. Подключаются они на каркасе: grpc.UnaryInterceptor для одного, grpc.ChainUnaryInterceptor для цепочки — первый в цепочке снаружи, как middleware роутера. Типовые jobs: recovery — поймать панику и вернуть codes.Internal вместо обрыва соединения; аутентификация — проверить токен в metadata и оборвать вызов до всякого декодирования; метрики уровня каркаса — счётчик grpc_server_handled_total по методам. Отсюда разница: ServerBefore работает ВНУТРИ Handler'а — когда метод уже выбран и ServeGRPC уже запущен, — он читает metadata в контекст и ни при каких условиях не может «отклонить» вызов. Интерсептор — снаружи, он видит сырой req и может не пустить handler дальше. Правило курса: то, что решает «пустить/не пустить» и ловит паники, — в интерсепторы; то, что поставляет данные в контекст для endpoint'а, — в ServerBefore. Метрики endpoint'а остаются в instrumentingMiddleware из урока 07-metriki, интерсептор их не заменяет, а дополняет транспортным уровнем.

Recovery-интерсептор

GO
func recoveryInterceptor(
        ctx context.Context,
        req interface{},
        info *grpc.UnaryServerInfo,
        handler grpc.UnaryHandler,
) (resp interface{}, err error) {
        defer func() {
                if r := recover(); r != nil {
                        logger.Log("panic", fmt.Sprint(r), "method", info.FullMethod)
                        err = status.Error(codes.Internal, "internal error")
                }
        }()
        start := time.Now()
        resp, err = handler(ctx, req)
        logger.Log("method", info.FullMethod, "took", time.Since(start).String())
        return resp, err
}

Сигнатура стоит заучивания: ctx и req — вход, info говорит, какой метод вызван, handler — продолжение цепочки, и вызвать его — твоя обязанность: не вызвал — вызов не случился. Без recovery паника в endpoint'е оборвала бы HTTP/2-соединение со всеми мультиплексированными вызовами; здесь она превращается в status.Error(codes.Internal, ...) и строку в лог из урока 06. took у интерсептора — время всего вызова каркаса, включая кодеки; она шире took из loggingMiddleware — сравни в логах, разница и есть цена транспорта. В main их два: grpc.ChainUnaryInterceptor(recoveryInterceptor, authInterceptor) — recovery снаружи, чтобы словить панику даже из auth. Для стриминговых методов есть зеркальный StreamInterceptor — он понадобится, когда дойдёт до потоков.

Клиент: conn как io.Closer — круг discovery замкнут#

Клиентская сторона — недостающая деталь фабрики из урока 12-discovery. grpc.Dial(target, opts...) строит grpc.ClientConn — виртуальное соединение с инстансом (лениво: реальный коннект происходит на первом вызове); вместо устаревшего WithInsecure передай grpc.WithTransportCredentials(insecure.NewCredentials()). Дальше grpctrans.NewClient(conn, &pb.Strings_ServiceDesc, "Uppercase", enc, dec, options...) возвращает Client с методом Endpoint() — исходящий вызов стал endpoint'ом, как и обещал клиентский транспорт из урока 04. ServiceDesc берётся сгенерированный: клиенту из него важны только ServiceName и MethodName — они склеятся в путь /strings.Strings/Uppercase, поля Handler нужны серверной диспетчеризации, до которой клиенту дела нет. Кодеки клиента зеркальны серверным: EncodeRequestFunc пакует UppercaseRequest в pb.UppercaseRequest, DecodeResponseFunc разворачивает *pb.UppercaseReply в UppercaseResponse. А теперь главная строчка: фабрика возвращает conn вторым значением — io.Closer. HTTP-фабрика возвращала nil, gRPC-фабрика честно отдаёт соединение: когда инстанс покраснел и ушёл из sd.Event, Endpointer вызовет Close у его фабричного результата — conn закрыт, ресурсы отданы. Дальше цепочка из урока 11 не меняется ни на символ: lb.NewRoundRobin и lb.Retry работают поверх gRPC-ног так же, как поверх HTTP. В новых версиях grpc-go вместо grpc.Dial рекомендуют grpc.NewClient — сигнатура та же, семантика чище.

gRPC-фабрика для sd.Factory

GO
func uppercaseGRPCFactory(instance string) (endpoint.Endpoint, io.Closer, error) {
        conn, err := grpc.Dial(instance, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                return nil, nil, err
        }
        e := grpctrans.NewClient(
                conn,
                &pb.Strings_ServiceDesc,     // сгенерированное описание сервиса
                "Uppercase",                 // метод склеится в /strings.Strings/Uppercase
                encodeGRPCUppercaseRequest,  // UppercaseRequest → *pb.UppercaseRequest
                decodeGRPCUppercaseResponse, // *pb.UppercaseReply → UppercaseResponse
                grpctrans.ClientBefore(
                        requestIDToMetadata(),
                        kitot.ContextToGRPC(tracer, logger),
                ),
        ).Endpoint()
        return e, conn, nil // conn — io.Closer: Endpointer закроет при вычёркивании инстанса
}

Фабрика из урока 12 в gRPC-редакции: контракт func(instance string) (endpoint.Endpoint, io.Closer, error) исполнен дословно, и Closer наконец не nil. Клиентские опции повторяют серверную пару: request_id и спан едут в metadata каждого исходящего вызова — трейс склеит прокси и соседа по uber-trace-id, как в уроке 08. Заметь, кодеки клиента — это кодеки сервера, вывернутые наизнанку: encodeGRPCUppercaseRequest/decodeGRPCUppercaseResponse описаны в клиентском тексте выше. Подставь эту фабрику вместо uppercaseFactory в консул-примере урока 12 — и балансированная цепочка начнёт говорить protobuf, не переписав ни обёртки, ни прокси-middleware. В проде добавь в Dial таймаут-опции и крипто-креды: insecure — только внутри доверенной сети.

Карта gRPC-транспорта go-kit#

ЭлементПакетКонтрактРоль
grpctrans.NewServerkit/transport/grpc(ep, dec, enc, options...) → *Serverendpoint в Handler со своими кодеками
grpctrans.Handlerkit/transport/grpcServeGRPC(ctx, req) (ctx, interface{}, error)стык каркаса gRPC с endpoint'ом
DecodeRequestFunckit/transport/grpcfunc(ctx, interface{}) (interface{}, error)*pb.UppercaseRequest → UppercaseRequest
EncodeResponseFunckit/transport/grpcfunc(ctx, interface{}) (interface{}, error)UppercaseResponse → *pb.UppercaseReply
ServerErrorEncoderkit/transport/grpcfunc(ctx, err) errorerr канала → status.Error с кодом
ServerRequestFunc / ClientRequestFunckit/transport/grpcfunc(ctx, *metadata.MD) ctxmetadata ↔ контекст: request_id, спан
grpctrans.NewClientkit/transport/grpc(conn, ServiceDesc, method, enc, dec)исходящий endpoint поверх gRPC
grpc.ChainUnaryInterceptorgoogle.golang.org/grpcfunc(ctx, req, info, handler)recovery, auth, метрики уровня каркаса

Порядок внедрения gRPC-транспорта#

  1. Описать .proto-контракт по операциям интерфейса сервиса; Err-поле — строкой, номера полей — сразу как контракт.
  2. Сгенерировать pb-пакет: protoc с protoc-gen-go и protoc-gen-go-grpc, go_package с путём к pb.
  3. Написать пары gRPC-кодеков: decodeGRPCRequest и encodeGRPCResponse по паре на метод.
  4. Собрать grpcServer со встроенным Unimplemented и Handler'ами через grpctrans.NewServer; конструктор NewGRPCServer от endpoint'ов.
  5. Подключить ServerBefore: requestIDToContext и kitot.GRPCToContext; ServerErrorEncoder — таблица кодов.
  6. Каркас: net.Listen :8082, grpc.NewServer с ChainUnaryInterceptor (recovery, auth), pb.RegisterStringsServer, Serve.
  7. Клиент: фабрика с grpc.Dial + grpctrans.NewClient и conn как io.Closer — подставить в цепочку sd из уроков 11-12.
  8. Проверить grpcurl'ом оба метода и убедиться, что логи, метрики и спаны из уроков 06-08 видят gRPC-вызовы.

Что возвращает grpctrans.NewServer и как он встаёт в каркас gRPC?

Endpoint вернул err — context.DeadlineExceeded, ServerErrorEncoder не задан. Что увидит клиент?

Куда класть аутентификацию по токену и recovery — в ServerBefore или в интерсептор каркаса?

Один endpoint, два транспорта

Прочитайте код и выберите, что он выведет:

uppercase := makeUppercaseEndpoint(svc)
uppercase = loggingMiddleware(logger)(uppercase)      // урок 06
uppercase = instrumentingMiddleware(uppercase)        // урок 07

// HTTP на :8081 (урок 04) и gRPC на :8082 (этот урок)
// едят ОДИН И ТОТ ЖЕ uppercase.
// Пришёл вызов по gRPC — что окажется в логах и метриках?

Инстанс покраснел — что с conn?

Прочитайте код и выберите, что он выведет:

// Консул-цепочка из урока 12, фабрика — gRPC из этого урока:
// возвращает endpoint и conn как io.Closer.
// Инстанс перестал проходить health check и исчез из sd.Event.

Собери серверную и клиентскую половины

Восстанови четыре строки. Пропуски — по порядку: конструктор Handler'а в kit/transport/grpc, метод Handler'а, сгенерированная функция регистрации, конструктор соединения каркаса.

// серверная половина: upper := grpctrans.(uppercase, decodeGRPCUppercaseRequest, encodeGRPCUppercaseResponse) func (s *grpcServer) Uppercase(ctx context.Context, req *pb.UppercaseRequest) (*pb.UppercaseReply, error) { _, rep, err := s.uppercase.(ctx, req) if err != nil { return nil, err } return rep.(*pb.UppercaseReply), nil } // main: baseServer := grpc.NewServer() pb.(baseServer, NewGRPCServer(uppercase, count)) // клиентская половина — фабрика из урока 12: conn, err := grpc.(instance, grpc.WithTransportCredentials(insecure.NewCredentials()))

Путь gRPC-вызова

Расставь этапы вызова Uppercase по gRPC: от grpcurl до ответа.

Строки

Ваша программа

Нажимайте на строки слева, чтобы собрать программу

Термины урока

Термин

grpctrans.Handler

нажмите, чтобы перевернуть
Определение

Интерфейс ServeGRPC(ctx, request) (context.Context, interface{}, error): стык каркаса gRPC с endpoint'ом; возвращается grpctrans.NewServer.

1 / 6

Что дальше#

Транспортная глава закрыта: endpoint из урока 02-endpoint обзавёлся вторым лицом — HTTP из урока 04-http-transport и gRPC из этого урока едят один и тот же набор с обёртками из уроков 06-10, а gRPC-клиент встал в фабрику из урока 12 и замкнул круг discovery. Освежить кодеки — в уроке 05-codeki, балансировку с ретраями — в уроке 11-lb-proxy; карта курса — на странице курса. Дальше два урока про middleware как самостоятельный слой: сначала урок 14-endpoint-middleware — логи и метрики как endpoint-обёртки, тот же instrumentingMiddleware, но собранный по правилам пакета endpoint; затем урок 15-service-middleware — декораторы сервиса и инъекция зависимостей. Транспорт может меняться, слои — нет: в этом и был план.

Песочница урока

Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.

Вывод

Запустите код, чтобы увидеть вывод.

Коротко: что запомнить

  • gRPC в go-kit — ещё один плагин транспорта: тот же endpoint, те же обёртки, только ноги — бинарный protobuf поверх HTTP/2.
  • Контракт объявляется в .proto: service Strings, rpc-методы, message-типы; protoc с protoc-gen-go и protoc-gen-go-grpc выдаёт pb-пакет, интерфейс StringsServer и Strings_ServiceDesc.
  • grpctrans.NewServer(ep, dec, enc, options...) возвращает grpctrans.Handler — ServeGRPC(ctx, req); склейку с каркасом делает твой grpcServer со встроенным UnimplementedStringsServer.
  • Кодеки перекладывают pb-структуры в доменные пары из урока 02 и обратно; сериализацией занимается сам каркас.
  • Бизнес-ошибка — в Err-поле reply по канону Failer; err канала — в gRPC-статус: DeadlineExceeded каркас конвертирует сам, остальное настраивает ServerErrorEncoder.
  • Metadata = заголовки gRPC: ServerBefore/ClientBefore гонят request_id и спан через контекст; kitot.GRPCToContext и ContextToGRPC склеивают трейс.
  • Интерсепторы каркаса (recovery, auth, метрики вызова) — вокруг всего вызова; ServerBefore — внутри Handler'а и только поставляет данные.
  • grpctrans.NewClient(conn, &pb.Strings_ServiceDesc, method, enc, dec).Endpoint() — исходящая нога; в sd.Factory conn возвращается io.Closer и закрывается вместе с инстансом.

Домашнее задание

Переведи stringsvc на gRPC-ноги. Первое: опиши stringsvc.proto по уроку, сгенерируй pb-пакет командой protoc с paths=source_relative и убедись, что в pb-папке появились UppercaseRequest, StringsServer и Strings_ServiceDesc. Второе: напиши gRPC-кодеки и grpcServer, подними второй слушатель на :8082 рядом с HTTP из урока 04 и проверь grpcurl'ом оба метода — в логах из урока 06 должны появиться те же request_count и took, что и от HTTP-вызовов. Третье: добавь ServerErrorEncoder с тремя ветками (DeadlineExceeded, sentinel твоего сервиса, Internal) и через grpcurl вызови метод с пустой строкой — убедись, что приедет err в reply, а не статус. Четвёртое: напиши requestIDToContext и requestIDToMetadata, прогони вызов с заголовком x-request-id через прокси из урока 11 на gRPC-фабрике из урока 12 — request_id должен дожить до логов соседа. Пятое: оберни каркас ChainUnaryInterceptor(recovery, auth) и проверь оба сценария: вызов без токена отваливается до endpoint'а, а паника в тестовом endpoint'е возвращается как codes.Internal без обрыва соединения; запиши, где в цепочке оказались твои логи.

Личные заметкиПусто
Хранятся только в вашем браузере (go-kit · gRPC транспорт в go-kit)

Вопросы и ответы по уроку

ServerBefore или grpc.UnaryInterceptor — что использовать для аутентификации?

Интерсептор: аутентификация решает «пустить/не пустить» и должна сработать до декодирования и до Handler'а — интерсептор видит info.FullMethod и может не вызвать handler, вернув codes.Unauthenticated. ServerBefore работает внутри ServeGRPC, когда метод уже выбран: он умеет только поставлять данные в контекст. Рабочая связка: auth-интерсептор проверяет токен и кладёт идентичность в контекст, ServerBefore дальше дообогащает контекст request_id и спаном.

Нужен ли gRPC, если внешний API остаётся HTTP/JSON?

Это не «или/или»: в go-kit оба транспорта едят одни endpoint'ы, и типовая схема — HTTP/JSON наружу для людей и партнёров, gRPC между сервисами кластера, где важны контракт в .proto, кодогенерация клиентов и бинарная экономия. Запускай оба слушателя в одном main: логи, метрики, спаны, лимитеры и breaker'ы общие. Если же сервис ходит только в браузер или отдаёт вебхуки, gRPC ничего не даст — grpc-web решает проблему браузера, но добавляет слой прокси.

grpc.Dial или grpc.NewClient — что использовать в фабрике?

В новых версиях grpc-go grpc.Dial считается устаревшим именем: grpc.NewClient делает то же самое с честной семантикой — сразу возвращает ленивый ClientConn, не пытаясь устанавливать соединение в момент вызова. Для существующего кода с grpc.Dial ничего не ломается — сигнатуры совпадают. Ключевое для фабрики из урока 12 не меняется: conn ленив, первый вызов устанавливает соединение, а закрытие — забота io.Closer, который Endpointer вызовет при вычёркивании инстанса.

Термины урока

Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.

Следующая ступень учебного пути

gRPC25 уроков~14 чпродвинутый25 уроков уже доступны

gRPC-Go: высокопроизводительный RPC

Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.

  • Писать .proto-схемы и генерировать код
  • Реализовывать unary и streaming сервисы
  • Управлять метаданными, дедлайнами и отменами
  • Строить interceptors для логов и аутентификации