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

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

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

Урок 15 из 25 ~18 мин +50 XP

Мидлвари сервисов

Endpoint-цепочка из урока 14 закрыла этаж пути запроса, но у go kit service middleware есть своя территория: всё, что знает домен — кэш по ключу строки, ретраи к хранилищу, метрики бизнес-событий, логи уровня операций. type ServiceMiddleware func(Service) Service и декораторы сервиса со встраиванием интерфейса — тот же паттерн, что loggingMiddleware из урока 03, только теперь по всем правилам: композиция матрёшкой в main, порядок «кто снаружи — тот первый», инъекция зависимостей через конструкторы и честная граница с endpoint-обёртками. Ты напишешь кэширующий декоратор stringsvc, соберёшь applyMiddleware для среза обёрток, поставишь прокси из урока 11 на его законное место service-этажа и поймёшь, почему домен остаётся чистым даже после пяти слоёв обёрток: декоратор go-кода повторяет контракт интерфейса, а не расширяет его.

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

Второй этаж: зачем обёрткам интерфейса своя глава#

Урок 03 подарил тебе первый декоратор — LoggingMiddleware вокруг StringService — и обещал «разложить по полочкам» в отдельном уроке. Полочки такие. Во-первых, паттерн: type ServiceMiddleware func(StringService) StringService — тип-псевдоним, конструктор с зависимостями, структура со встраиванием интерфейса и переопределением нужных методов; один лекал закрывает кэш, ретраи, бизнес-метрики и прокси. Во-вторых, композиция: декораторы навешиваются матрёшкой в main до создания endpoint'ов, и порядок этой матрёшки — самостоятельное инженерное решение со своими правилами. В-третьих, инъекция зависимостей: logger, хранилище, кэш входят через конструкторы, никакого глобального состояния — урок 03 это заложил, здесь доведём до среза обёрток и функции применения. В-четвёртых, граница: что уносится в декоратор, а что остаётся endpoint-обёртке из урока 14. Прокси из урока 11-lb-proxy — proxymw и proxyingMiddleware — окажется частным случаем того же паттерна, и после этого урока все обёртки курса встанут в единую систему из двух этажей. Ключ к слою один: go kit service middleware живёт над интерфейсом домена и не знает про транспорт ничего.

Type ServiceMiddleware: контракт обёртки интерфейса#

Начни с определения, которое нужно запомнить дословно: type ServiceMiddleware func(StringService) StringService. Декоратор — функция, принимающая сервис и возвращающая сервис; сигнатура не меняется, поэтому main может навесить хоть пять штук подряд, и тип останется StringService. Отсюда первое отличие от endpoint-этажа: там существовал универсальный endpoint.Middleware, пригодный к любому endpoint'у; здесь универсального типа в пакете нет — у каждого сервиса свой псевдоним со своим интерфейсом. Это не неудобство, а честность: декоратор сервиса связан с доменом сильнее, чем endpoint-обёртка с запросом, и компилятор обязан это видеть. Второе отличие — момент исполнения: endpoint-обёртка применялась к готовой функции; service-декоратор применяется к реализации интерфейса ДО построения endpoint'ов — makeUppercaseEndpoint из урока 02 получает уже декорированный svc и про обёртки не знает. Третье отличие — видимость: декоратор видит доменные аргументы и может принимать решения по их содержимому («пустая строка — не кэшировать»), endpoint-обёртка видит только ctx и interface{}. Именно поэтому кэш из этого урока невозможен наверху, а лимитер из урока 09 — невозможен внизу: у каждого этажа свои вопросы.

Лекал декоратора: тип, конструктор, встраивание

GO
type ServiceMiddleware func(StringService) StringService

// Конструктор с зависимостью — по канону урока 03.
func NewLoggingMiddleware(logger log.Logger) ServiceMiddleware {
        return func(next StringService) StringService {
                return loggingMiddleware{next, logger}
        }
}

type loggingMiddleware struct {
        StringService // встраивание: Count наследуется, Uppercase переопределён
        logger        log.Logger
}

func (mw loggingMiddleware) Uppercase(ctx context.Context, s string) (output string, err error) {
        defer func(begin time.Time) {
                mw.logger.Log("method", "uppercase", "input", s,
                        "output", output, "err", err, "took", time.Since(begin))
        }(time.Now())
        output, err = mw.StringService.Uppercase(ctx, s)
        return
}

Три составные части лекала. Псевдоним типа — контракт обёртки; конструктор принимает зависимость и возвращает декоратор; структура со встраиванием переопределяет только то, что ей нужно. Встраивание StringService даёт бесплатное делегирование: метод Count не переопределён, поэтому вызов уходит прямо в обёрнутый сервис — декоратор «прозрачен» для остальных методов. Named returns output и err видны в defer — тот же приём, что в loggingMiddleware урока 06, только на домене.

Своя обёртка №1: кэш над методом домена#

Пора за рукотворное. Кэш — хрестоматийный resident service-этажа: он знает, что ключ — это строка s из домена, и решает «отвечать или считать», меняя саму суть вызова; endpoint-обёртка на это не способна в принципе, ведь request-структуру ей трогать запрещено правилами урока 14. Реализация — тот же лекал: структура с встроенным StringService, картой и RWMutex, переопределённый Uppercase. Обрати внимание на два решения внутри. Первое: кэшируем только успешные вычисления — ошибка ErrEmpty не попадает в карту, иначе пустой ввод один раз закэшируется и сервис навсегда перестанет его валидировать. Второе: Count не кэшируется вообще — переопределений меньше, поведение прозрачнее; захочешь — допишешь по тому же лекалу. RWMutex вместо Mutex: чтения много, записи редки — read-блокировка параллелит читателей. Кэш здесь in-memory для простоты; продовый вариант прячет хранилище за интерфейсом из урока 03 и подставляет Redis-реализацию тем же конструктором — инъекция зависимостей в действии.

cachingMiddleware: map под RWMutex

GO
type cachingMiddleware struct {
        StringService
        cache map[string]string
        mtx   sync.RWMutex
}

func NewCachingMiddleware(cache map[string]string) ServiceMiddleware {
        return func(next StringService) StringService {
                return cachingMiddleware{next, cache, sync.RWMutex{}}
        }
}

func (mw cachingMiddleware) Uppercase(ctx context.Context, s string) (string, error) {
        mw.mtx.RLock()
        v, ok := mw.cache[s]
        mw.mtx.RUnlock()
        if ok {
                return v, nil // вызов base не случился вовсе
        }
        v, err := mw.StringService.Uppercase(ctx, s)
        if err != nil {
                return "", err // ошибки не кэшируются
        }
        mw.mtx.Lock()
        mw.cache[s] = v
        mw.mtx.Unlock()
        return v, nil
}

Сигнатура метода повторяет интерфейс: декоратор go-кода реализует контракт целиком, отличаясь лишь поведением вокруг base. Проверка «ок» под RLock отдаёт значение без вызова обёрнутого сервиса — вот почему декораторы относят к service-этажу: они меняют семантику вызова. Ключ карты — сама строка s, доменный аргумент; никакой сериализации и никакой возни с request-структурами, которых здесь просто нет.

Что увидит второй вызов

svc := NewLoggingMiddleware(logger)(NewCachingMiddleware(m)(stringService{})). Первый вызов Uppercase(ctx, "go") вернул "GO". Что произойдёт при втором Uppercase(ctx, "go")?

Композиция матрёшкой: порядок — это решение#

Декораторы навешиваются последовательно: svc = NewLoggingMiddleware(logger)(svc), затем svc = NewCachingMiddleware(m)(svc), и так далее. Читается сборка изнутри наружу: самый последний надетый декоратор стоит снаружи и встречает вызов первым — та же арифметика, что у ручной обёртки endpoint'ов из урока 02. Каноническое правило уроков 06-10 «кто внутри — тот пишет первым» работает и здесь: defer внутреннего декоратора сработает раньше defer внешнего. Как выбрать порядок? Задай вопрос, что должно быть видимым. Логи снаружи кэша видят и попадания, и промахи — дежурный видит полный трафик; логи внутри видят только реальные вычисления, зато took показывает чистую стоимость базы. Метрики бизнес-событий обычно ставят самым внешним слоем: счётчик должен считать каждый вызов, включая кэш-хиты. Кэш — самым внутренним, перед самой реализацией: его место — стереть вызов, а не пропустить его через все слои. Прокси из урока 11 в stringsvc3 стоит снаружи метрик — запрос сначала решает «я или сосед», и только потом уходит в подсчёты. Каждая перестановка — легальна и осмысленна; документируй выбор комментарием в main, как это делалось с порядком endpoint-цепочки.

Переставь матрёшку

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

// Вариант А:
svc := NewLoggingMiddleware(logger)(NewCachingMiddleware(m)(base))

// Вариант Б:
svc := NewCachingMiddleware(m)(NewLoggingMiddleware(logger)(base))

// Второй вызов Uppercase(ctx, "go") — что различится?

Прокси из урока 11 — тот же паттерн#

Теперь у тебя есть карта, и proxymw из урока 11-lb-proxy встаёт на место без сюрпризов: proxyingMiddleware — это service-декоратор, структура с встроенным StringService, чей переопределённый Uppercase зовёт makeUppercaseProxy и иногда отвечает чужим ответом. Почему прокси — service-этаж, хотя он ходит в сеть? По двум нашим же проверочным вопросам: он знает домен (перенаправляет конкретный метод с конкретной строкой) и меняет семантику вызова (отвечает вместо локального вычисления) — оба ответа указывают на декоратор. При этом клиентская нога прокси — endpoint из kithttp.NewClient, — обёрнута в свой endpoint-цепочку с breaker'ом и метриками: этажи не смешиваются, каждый работает со своим слоем абстракции. Именно так канонический stringsvc3 собирает main: базовая реализация, поверх — прокси-декоратор, поверх — логи и метрики сервиса, затем endpoint'ы, затем endpoint-middleware из урока 14, затем транспорты из уроков 04 и 13. Двухэтажная система обёрток — главное архитектурное наследие курса; запомни порядок сборки, он понадобится в итоговом проекте урока 25.

Сборка stringsvc: двухэтажная система

GO
var svc StringService
svc = stringService{}

// Этаж service: декораторы домена, снаружи — первый встречает вызов.
svc = NewLoggingMiddleware(logger)(svc)        // бизнес-логи — снаружи
svc = NewCachingMiddleware(cache)(svc)         // кэш — перед базой

// Этаж endpoint: из декорированного сервиса строятся функции.
uppercase := makeUppercaseEndpoint(svc) // урок 02
uppercase = loggingMiddleware(logger)(uppercase) // этаж 14 и далее...

// Транспорт (урок 04) получает уже всё:
// httptransport.NewServer(uppercase, decode, encode, options...)

Две строки декораторов делают то, что в меньших фреймворках размазано по хендлерам: домен остался чистым — stringService{} про кэш и логи не знает. Заметь последовательность: сначала service-матрёшка, потом endpoint'ы, потом endpoint-цепочка. Обратный порядок невозможен: endpoint'ы едят готовый интерфейс, и декоратор, надетый после построения endpoint'ов, до них уже не дотянется — в функции закрыт старый svc.

Инъекция зависимостей: конструкторы вместо глобального состояния#

Урок 03 сформулировал правило: зависимости входят через конструкторы, глобальное состояние запрещено. Service-middleware — главный потребитель этого правила: logger, карта кэша, репозиторий, лимит хранилища — всё приходит параметрами конструктора, и декоратор не имеет права заводить свои пакеты-глобалы. Что это даёт на практике. Тестируемость: в тесте подставляется fake-логгер или пустая карта, и декоратор тестируется изоляционно — это будет кодом урока 16. Конфигурируемость: одна и та же матрёшка собирается с Redis-кэшем или с in-memory по флагу запуска. Явность: по сигнатуре NewCachingMiddleware видно, что обёртке нужен кэш, — ничего не прячется в пакетных переменных. Когда декораторов становится больше пяти, сборка в main превращается в лестницу из одинаковых строк; на этот случай держи функцию применения среза — applyMiddleware из следующего блока. Она — единственное место, где список декораторов и порядок их применения живут вместе, и её же удобно передавать в тесты: тест собирает матрёшку из своих стабов той же функцией, что и прод-код.

applyMiddleware: срез декораторов одним вызовом

GO
func applyMiddleware(svc StringService, mws ...ServiceMiddleware) StringService {
        for i := len(mws) - 1; i >= 0; i-- {
                svc = mws[i](svc)
        }
        return svc
}

// main:
svc := applyMiddleware(
        stringService{},
        NewLoggingMiddleware(logger), // снаружи
        NewCachingMiddleware(cache),
) // svc остаётся StringService

// Тест — та же функция, свои стабы:
fake := applyMiddleware(fakeService{}, NewCachingMiddleware(map[string]string{}))

Обход с конца — семантика «первый элемент среза снаружи», знакомая по endpoint.Middlewares из урока 02: два вида обёрток курса ведут себя одинаково, и это не совпадение, а сознательный дизайн. Срез декораторов удобно собирать по конфигу: включён кэш — добавляем декоратор в срез; нет — не добавляем, и ни один вызов в коде не меняется. Функция возвращает тот же интерфейс — сигнатура main не зависит от числа слоёв.

Порядок применения среза

applyMiddleware(base, A, B, C) обходит срез с конца: C(B(A(base))). Кто встретит вызов первым и кто последний?

Что уносим в декораторы, а что оставляем#

Границу урока 14 теперь можно расписать по списку. В service-декораторы уносится всё, что понимает домен: кэш по ключу, ретраи к хранилищу, метрики бизнес-событий («сколько строк посчитано»), логи уровня операций с доменными полями, лимиты на доменные сущности, прокси к другим инстансам. В endpoint-обёртках остаются путь запроса и наблюдаемость: логи method/err/took, request_count, спаны, токен-бакет, предохранитель, дедлайн, recovery. Есть серые зоны — проговори их честно. Ретраи: к внешнему хранилищу — декоратор (он знает операцию и её идемпотентность); к балансированным инстансам — lb.Retry на endpoint-этаже из урока 11, потому что это вопрос пути запроса. Метрики: транспортные (латентность запроса) — endpoint; бизнес-события (посчитано строк) — декоратор. Логи: на каждый вызов с method/err/took — endpoint; с input/output домена — декоратор. Третий критерий для сомнительных случаев: попробуй протестировать обёртку без транспорта и без endpoint'а, только с fake-сервисом; если тест выразился естественно — место декоратора верное, если потребовался ctx с request-структурой — это endpoint-этаж.

Два этажа обёрток рядом#

СвойствоEndpoint middlewareService middleware
Типendpoint.Middleware — единый для всехtype ServiceMiddleware — по одному на интерфейс
Что видитctx и interface{} запросадоменные аргументы и возвраты
Применяется кготовому endpoint-у в mainреализации интерфейса до endpoint'ов
Меняет семантикудоступ к вызову (лимит, breaker)сам вызов (кэш, прокси, ретраи)
Примеры курсалоги 06, метрики 07, breaker 10логи 03, прокси 11, кэш 15
Тестfake-next замыканиемfake-сервис без транспорта
КомпозицияChain / Middlewares / ручнаяматрёшка / applyMiddleware

Ретраи как декоратор: осторожно с идемпотентностью#

Второй хрестоматийный декоратор — ретраи — требует одной оговорки, которой не было у кэша. Повтор вызова Uppercase безопасен: операция чистая, строку в uppercase можно переводить сколько угодно раз. Но декоратор, повторяющий произвольные операции, — мина: если метод списывает деньги или публикует событие, повтор после частичного сбоя удвоит эффект. Правило то же, что и в lb.Retry из урока 11: ретраи только идемпотентных операций, и знание об идемпотентности живёт в домене — значит, и ретраи к хранилищу пишутся декоратором, который переопределяет только безопасные методы. Реализация — лекал, знакомый по кэшу: структура со встроенным интерфейсом, переопределённый метод, цикл из трёх попыток с паузой. Дополнение из урока 10: ретраи поверх breaker'а не ставятся никогда — обёртка не знает, где живёт предохранитель, и повтор может добить полуживую цепь; на нижнем этаже лимитер и breaker уже решают, кому идти, а декоратор получает только доброкачественный трафик. Если операция не идемпотентна — добавь ей идемпотентность (idempotency key в аргументах) или оставь единственную попытку.

Место для ретраев

Сервис ходит в Postgres через интерфейс Storage, методы GetProfile (чистое чтение) и AddOrder (пишет строку). Где ретраи уместны?

Домен остаётся чистым: итоговая карта#

Пройди глазами всю систему: stringService{} реализует Uppercase и Count — и не импортирует ничего из go-kit, кроме контекста; декораторы реализуют тот же интерфейс поверх, ничего не добавляя к контракту; endpoint'ы из урока 02 адаптируют методы к универсальной сигнатуре; endpoint-обёртки из уроков 06-14 обвешивают функции; транспорты из уроков 04 и 13 сериализуют. Каждый слой знает только соседей, и контракт между слоями — интерфейс сервиса и тип Endpoint. Именно это имел в виду урок 01-filosofiya, обещая «инструменты, а не каркас»: ни один слой не придётся переписать при смене соседа — смена транспорта не трогает декораторы, добавление кэша не трогает endpoint'ы, новый декоратор не трогает домен. Проверка на чистоту проста и красива: pkg-каталог сервиса собирается без единого импорта go-kit — если grep находит в домене kitlog или kitratelimit, слой протёк. Чек-лист декоратора из следующего блока — финальная полировка перед тестами урока 16, где вся эта матрёшка будет проверена изоляционно.

Чек-лист сервисного декоратора#

  • Сигнатура: type-псевдоним func(XService) XService и конструктор с зависимостями в параметрах.
  • Встраивание интерфейса: переопределяй только нужные методы, остальные — прозрачно в base.
  • Named returns и defer — если декоратор пишет лог или меряет время.
  • Состояние — под мьютексом; карта кэша, счётчики — поточно-безопасны.
  • Ошибки домена не глотаются: sentinel ErrEmpty и другие доходят до клиента неизменными.
  • Порядок матрёшки документирован в main: что видно в логах, что считает метрика.
  • Домен не импортирует go-kit: проверка grep'ом на pkg-каталог.

Sentinel сквозь два декоратора

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

// base возвращает ErrEmpty на пустой строке
svc := NewLoggingMiddleware(logger)(NewCachingMiddleware(m)(base))
_, err := svc.Uppercase(ctx, "")
_, err2 := svc.Uppercase(ctx, "")

Собери декоратор кэша

Восстанови пропуск в cachingMiddleware и в сборке матрёшки.

func (mw cachingMiddleware) Uppercase(ctx context.Context, s string) (string, error) { mw.mtx.() v, ok := mw.cache[s] mw.mtx.RUnlock() if ok { return v, nil } v, err := mw.StringService.(ctx, s) if err != nil { return "", err } mw.mtx.Lock() mw.cache[s] = v mw.mtx.Unlock() return v, nil } // main: svc := NewLoggingMiddleware(logger)(New(cache)(stringService{}))

Путь вызова Uppercase через два этажа

Расставь этапы вызова от HTTP-декодера до реализации.

Строки

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

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

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

Термин

ServiceMiddleware

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

type-псевдоним func(StringService) StringService: декоратор принимает и возвращает интерфейс; своего универсального типа в пакете endpoint нет — по одному на сервис.

1 / 6

Что дальше#

Система обёрток курса собрана полностью: endpoint-этаж из урока 14 и service-этаж этого урока закрывают все сквозные задачи stringsvc, а домен остался чистым — как обещал урок 03. Осталось научиться всё это проверять. Урок 16-testirovanie — про тестирование go kit: unit-тесты endpoint'ов прямыми вызовами, fake-next для endpoint-обёрток, fake-сервис для декораторов, кодеки через httptest.NewRecorder и интеграционные тесты транспорта. После него карта курса ведёт в сборку большого сервиса — уроки 17-18 про структуру и конфигурацию; путь до туда — на странице курса.

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

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

Вывод

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

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

  • Service middleware — type-псевдоним func(StringService) StringService: декоратор принимает и возвращает интерфейс; сигнатура сервиса не меняется ни после одного слоя.
  • Лекал декоратора: конструктор с зависимостями, структура со встраиванием интерфейса, переопределение только нужных методов, named returns + defer для логов и замеров.
  • Кэш — хрестоматийный декоратор: ключ из домена, RWMutex, ошибки не кэшируются; он меняет семантику вызова, что endpoint-обёртке запрещено.
  • Порядок матрёшки — решение о наблюдаемости: логи снаружи видят кэш-хиты, кэш внутри стирает вызовы до остальных слоёв; прокси из урока 11 — тот же паттерн.
  • applyMiddleware со срезом декораторов повторяет семантику endpoint.Middlewares: обход с конца, первый элемент снаружи.
  • Ретраи пишутся декоратором только для идемпотентных методов; поверх breaker'а ретраи не ставятся.
  • Граница этажей: доменные поля и смена семантики — декораторам, путь запроса и наблюдаемость — endpoint-обёрткам; домен не импортирует go-kit.

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

Дострой service-этаж stringsvc руками. Первое: реализуй cachingMiddleware по уроку, собери матрёшку логи-снаружи-кэш-внутри и прогони Uppercase с одной строкой десять раз — в логе должно быть десять записей с копеечным took после первой. Второе: переставь матрёшку кэш-снаружи-логи-внутри и запиши, что изменилось в наблюдаемости; комментарий в main с обоснованием финального порядка обязателен. Третье: напиши applyMiddleware, собери декораторы из среза по флагу -cache и убедись, что тест декоратора использует ту же функцию, что и main. Четвёртое: добавь retry-декоратор для Count с двумя попытками и проверь на fake-сервисе, что первая неудача перезапускается; затем объясни письменно, почему для AddOrder-аналога такой декоратор запрещён. Пятое: прогони grep по сервисному пакету на импорты go-kit — в домене их быть не должно; один найденный импорт контекста stdlib разрешён. Запиши получившуюся карту двух этажей и сверь её с порядком из уроков 14 и 11.

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

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

Почему у service middleware нет общего типа вроде endpoint.Middleware?

Endpoint универсален, потому что сигнатура Endpoint одна на всю систему — func(ctx, interface{}) (interface{}, error). Интерфейсы сервисов у всех разные: StringService несовместим с ProfilesService, и общий тип обёртки потребовал бы пустого interface{} на входе — возврат к тому, от чего go-kit уводит. Псевдоним на каждый сервис — type ServiceMiddleware func(StringService) StringService — сохраняет типобезопасность: декоратор от чужого интерфейса просто не скомпилируется.

Кэш-декоратор или кэш в сервисе — как выбрать?

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

Декоратор должен пробрасывать ctx или может его заменить?

Пробрасывать. Контекст принадлежит запросу и всему стеку: в нём лежат request_id из ServerBefore, спан из урока 08, дедлайны из timeout-обёртки. Декоратор, создающий context.Background(), отрежет нижним слоям и трассировку, и отмену — тихая поломка, которая проявится в Jaeger пропавшим деревом и в pprof — горутинами, которые никто не отменяет. Единственный легальный случай — фоновая задача, начатая с нового корневого контекста, и это отдельное осознанное решение.

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

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

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

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

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

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

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