---
title: "Middleware endpoints"
description: "Endpoint-middleware go-kit: анатомия func(Endpoint) Endpoint, свои обёртки recovery и timeout, порядок применения в цепочке и граница с декораторами сервиса."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/14-endpoint-middleware/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 18
---

# Middleware endpoints

*Курс «go-kit: микросервисный тулкит», урок 14 из 25. ~18 минут. Обновлено 21.09.2026. Источник: https://golangmaster.ru/courses/go-kit/lessons/14-endpoint-middleware/*

Уроки 06-10 выдали тебе пять готовых endpoint-обёрток — логи, лимит, спаны, метрики, предохранитель, — но между «поставить готовое» и «писать своё» лежит целая дисциплина. go kit endpoint middleware — это одна сигнатура func(endpoint.Endpoint) endpoint.Endpoint, три уровня вложенности функций и набор правил, которые отличают честную обёртку от свалки кода. В этом уроке ты соберёшь анатомию обёртки по слоям, напишешь три собственные — recovery, timeout и обогащение ошибок, — разберёшься, где каждая встаёт в каноничную цепочку stringsvc, почему тело фабрики исполняется один раз при сборке, а тело запроса — на каждый вызов, и где проходит граница с service middleware из следующего урока. Обёртки endpoint после этого перестанут быть магией из чужих пакетов: любая сквозная функция курса станет для тебя рукотворной.

### Пять обёрток уже есть: что осталось разобрать

Оглянись назад: [loggingMiddleware из урока 06](/courses/go-kit/lessons/06-logirovanie/), instrumentingMiddleware из [урока 07](/courses/go-kit/lessons/07-metriki/), kitot.TraceServer из [урока 08](/courses/go-kit/lessons/08-tracing/), NewErroringLimiter из [урока 09](/courses/go-kit/lessons/09-rate-limit/) и kitcb.Gobreaker из [урока 10](/courses/go-kit/lessons/10-circuit-breaker/) — пять работающих обёрток endpoint, и каждая собрана по одному лекалу. [Урок 02-endpoint](/courses/go-kit/lessons/02-endpoint/) дал три способа композиции — ручную обёртку, endpoint.Chain и срез endpoint.Middlewares. Осталось два пробела. Первый: все go kit endpoint middleware были готовыми, из коробки; свою пока не написал никто — а в реальном сервисе своих всегда больше, чем чужих. Второй: порядок сборки мы приняли как канон, но не вывели как правило, и новые обёртки — recovery, timeout, обогащение ошибок — в этот канон ещё не вписаны. Этот урок закрывает оба пробела: сначала анатомия и правила, затем три рукотворные обёртки, затем сборка полной цепочки stringsvc и граница с декораторами сервиса, которые ждут тебя в следующем уроке. Всё, что здесь появится, работает одинаково для HTTP и gRPC-транспорта из уроков 04 и 13 — в этом смысл слоя.

### Анатомия обёртки: три уровня вложенности функций

Любой endpoint middleware — это матрёшка из трёх функций, и понимание того, когда исполняется каждая, отделяет инженера от копипастера. Внешний уровень — фабрика: func(deps) endpoint.Middleware; она принимает зависимости (логгер, счётчики, бакет) и возвращает обёртку. Средний уровень — сама обёртка func(next endpoint.Endpoint) endpoint.Endpoint: она принимает обёрнутый endpoint и возвращает новый. Внутренний уровень — замыкание func(ctx, request) (interface{}, error): вот это и есть «endpoint с добавленным поведением», который исполняется на каждый запрос. Ключевое следствие: тело фабрики и тело обёртки исполняются один раз при сборке в func main, а тело замыкания — на каждый вызов. Всё дорогое — подключение к базе, парсинг конфига, создание клиента — должно случиться на первых двух уровнях и попасть в замыкание через захват переменных. Всё, что про конкретный запрос, — разбор ctx, вызов next, замер времени — живёт внутри. Сигнатура endpoint.Middleware объявлена в пакете endpoint как type Middleware func(Endpoint) Endpoint, и никаких других типов для обёрток не нужно: одна строка определения тянет весь слой, а kit обёртки этого сорта — логи, метрики, лимиты — различаются только телом замыкания.

### Скелет обёртки: от фабрики до замыкания

```go
import (
        "context"

        "github.com/go-kit/kit/endpoint"
)

// Уровень 1: фабрика — исполняется один раз при сборке в main.
func myMiddleware(cfg Config) endpoint.Middleware {
        // Здесь можно дорогое: открыть соединение, прогреть кэш.
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                // Уровень 2: обёртка — тоже один раз, при применении к endpoint.
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                        // Уровень 3: замыкание — исполняется на КАЖДЫЙ запрос.
                        // next — endpoint, обёрнутый всеми обёртками глубже текущей.
                        return next(ctx, request)
                }
        }
}

// Применение: ep = myMiddleware(cfg)(ep) — или через endpoint.Chain.
```

Три уровня — три разных момента жизни программы. Ошибка новичка — положить инициализацию на уровень 3: она будет повторяться на каждый запрос и съедать латентность. Вторая ошибка — забыть вызвать next: обёртка-разрушитель молча отбросит весь трафик, и endpoint за ней оживёт только после отката. Пока next не вызван, ни один из нижних слоёв не существует для запроса — в этом и сила перехвата.

### Шесть правил хорошей обёртки

- Не меняй типы: request и response проходят сквозь обёртку как есть — тип-assert это работа адаптеров из урока 02, а не middleware.
- Одна ответственность: логи, метрики, лимит — отдельно; обёртка «логи и метрики» невозможна к перенастройке и тестированию поодиночке.
- Именованные результаты: (response interface{}, err error) + defer — единственный способ увидеть ответ ПОСЛЕ вызова next, как это делают loggingMiddleware и instrumentingMiddleware из уроков 06-07.
- Состояние — только потокобезопасное: замыкание исполняют тысячи горутин параллельно; map под мьютексом или атомарный счётчик, а не голые поля.
- Ошибки — каноничные: возвращай sentinel или тип-носитель из урока 05, чтобы encodeError на транспорте умел их превращать в статусы, а breaker — в неудачи.
- Уважай контекст: читай значения, положенные ServerBefore (request_id из урока 06), не подменяй чужие ключи и не отменяй не свой cancel — ctx принадлежит всему стеку, а не обёртке.

### Своя обёртка №1: recovery — паника становится ошибкой

Первая рукотворная обёртка закрывает дыру, о которой вслух говорят редко: что будет с сервисом, если endpoint запаникует. Ответ честный и неприятный: каркас net/http ловит панику в ServeHTTP и обрывает соединение, каркас gRPC делает то же с codes.Internal — но ни логов с контекстом, ни метрик, ни честного статуса ты не получишь, а goroutine-утечка из-за недозакрытого ресурса может остаться навсегда. Recovery-обёртка решает это на слое endpoint: defer с recover() превращает панику в обычную ошибку err, и дальше всё работает по канону — лог из урока 06 запишет err, метрика из урока 07 инкрементирует error=true, breaker из урока 10 посчитает неудачу. Почему это endpoint-middleware, а не код в сервисе: паника — свойство всей цепочки вызовов, а не бизнес-логики; сервис из [урока 03](/courses/go-kit/lessons/03-service/) паниковать не должен, но чужой код в адаптере или сторонней библиотеке — может. И одно место вместо трёх: HTTP-транспорт и gRPC-транспорт получают защиту одновременно, писать по копии на транспорт не нужно — транспортная независимость, обещанная в уроке 01.

### recoveryMiddleware: defer + recover на замыкании

```go
import (
        "context"
        "fmt"
        "runtime/debug"

        "github.com/go-kit/kit/endpoint"
        "github.com/go-kit/log"
)

func recoveryMiddleware(logger log.Logger) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (resp interface{}, err error) {
                        defer func() {
                                if r := recover(); r != nil {
                                        logger.Log("err", fmt.Sprintf("panic: %v", r),
                                                "stack", string(debug.Stack()))
                                        err = fmt.Errorf("internal panic: %v", r)
                                }
                        }()
                        return next(ctx, request)
                }
        }
}
```

Нюанс, без которого код не работает: именованные результаты resp и err объявлены в сигнатуре замыкания. recover() возвращает значение только внутри отложенной функции той же горутины, и записать его надо именно в именованный err — анонимные результаты defer перехватить не может. Панячий response равен nil, поэтому каркас увидит только err и честно отработает ошибку. debug.Stack() стоит пары килобайт — оставляй его только в логах, не в ответе клиенту: стек раскрывает внутренности, которых наружу выносить нельзя.

### Куда встаёт recovery: самая внутренняя позиция

Правило простое: recovery ставится ближе всех к endpoint, сразу внутри breaker'а. Два аргумента. Первый: паника должна стать ошибкой ДО того, как её увидят остальные обёртки — breaker из урока 10 считает неудачей любой err от обёрнутого endpoint'а, и превращённая паника попадёт в Counts как честная поломка; если паника пролетит сквозь Execute, gobreaker не поймает её в счётчик, а каркас оборвёт соединение — предохранитель ослепнет. Второй: лог и метрики стоят снаружи — они увидят уже преобразованную ошибку с осмысленной строкой panic: и нормальный took вместо обрыва. Порядок из уроков 09-10 — logging, лимитер, трейсер, метрики, breaker — дополняется хвостом: breaker, recovery, endpoint. Заметь, что timeout, к которому мы перейдём, встаёт туда же, между breaker'ом и recovery: дедлайн — свойство вызова зависимости, и сработавший таймаут обязан быть виден предохранителю как неудача, иначе медленный сосед не откроет цепь. Обе новые обёртки живут в «защитном хвосте» цепочки — это третье правило порядка после «дешёвый отказ раньше» и «наблюдаемость снаружи».

### Своя обёртка №2: timeout — context.WithTimeout

```go
func timeoutMiddleware(d time.Duration) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                        ctx, cancel := context.WithTimeout(ctx, d)
                        defer cancel()
                        return next(ctx, request)
                }
        }
}

// В цепочке: после breaker'а, перед recovery.
// upper = timeoutMiddleware(2 * time.Second)(upper)
```

Обёртка в четыре строки — и самая коварная в курсе. context.WithTimeout возвращает производный контекст и функцию cancel: вызов cancel в defer освобождает ресурсы таймера немедленно, а не через время d — без него каждый запрос будет капать утечкой. Дедлайн распространяется вниз: если сервис вызывает базу или соседа через контекст, отмена прилетит и туда. Ловушка: WithTimeout ставит потолок, но не рвёт соединение — next обязан УВАЖАТЬ ctx (проверять ctx.Err(), передавать контекст дальше), иначе middleware бесполезна, как будильник без кнопки сброса.

### Дедлайн в err: что видят остальные слои

Разберём, что происходит при срабатывании. Пока длится next, клиентский вызов блокирован; в момент истечения d контекст отменяется, ctx.Err() становится context.DeadlineExceeded, и если код внутри (адаптер, сервис, клиент зависимости) проверяет контекст — замыкание вернёт nil-ответ и err = context.DeadlineExceeded. Дальше по цепочке вверх: breaker видит ошибку и — при дефолтном IsSuccessful из урока 10 — засчитывает неудачу, ведь дедлайн означает, что зависимость не успела; метрики фиксируют error=true и took около двух секунд; лог записывает err; трейсер закрывает спан с ошибкой; кодек из урока 05 отдаёт клиенту 503 или 504 через тип-носитель. Вся наблюдаемость работала потому, что timeout стоит в защитном хвосте, а не снаружи: поставь его первым, и предохранитель никогда не узнает о медленном соседе — обёртки молча гасили бы таймауты до breaker'а. Сравни с транспортными ручками: http.Server.WriteTimeout и gRPC-дедлайн из metadata решают задачу на каркасе и не видны endpoint-слою; endpoint-обёртка — это страховка на уровне операции, транспортная независимость в чистом виде.

### Тонкий случай дедлайна

```go
ep := timeoutMiddleware(100 * time.Millisecond)(base)
// base внутри делает time.Sleep(1 * time.Second) и возвращает ("ok", nil)

resp, err := ep(context.Background(), UppercaseRequest{S: "go"})
```

**Что выведет программа?**

- resp = "ok", err = nil — Sleep всё равно доспит
- ✅ resp = nil, err = context.DeadlineExceeded
- Горутина паникует на отмене контекста
- ep зависнет навсегда, потому что ctx не отменяется

base возвращает ok через секунду, но к этому моменту замыкание уже не существует: WithTimeout отменил производный контекст, и УВАЖАЮЩИЙ контекст код в base вышел по ctx.Err(). В реальном коде Sleep заменяет база, которая проверяет контекст — если бы она его игнорировала, вернулся бы вариант 1: middleware не рвёт выполнение, она только объявляет дедлайн.

### Своя обёртка №3: обогащение ошибок именем метода

Третья обёртка демонстрирует, что middleware может менять не только поведение, но и саму ошибку, не трогая response-структуры. Проблема: после логов, метрик и breaker'а в err может прилететь голая «connection refused» без намёка, какой метод сервиса пострадал; по логам Uppercase и Count различить несложно, но в системе с двадцатью endpoint'ами контекст ошибки стоит того, чтобы добавить его централизованно. Решение — обёртка, которая оборачивает ошибку через fmt.Errorf с глаголом %w: каноническая ошибка остаётся извлекаемой через errors.Is (лимитер, breaker и encodeError из уроков 09-10 продолжают работать), а поверх появляется префикс с именем метода. Этот приём — стандартная идиома Go, и в endpoint-мире он особенно удобен: одна обёртка на все методы вместо правки каждого адаптера. Заодно она показывает паттерн «обёртка-трансформер»: next вызывается, результат проверяется, при необходимости — модифицируется и возвращается дальше. Такие обёртки пишутся минут за десять, а экономят часы дежурств: дежурный видит в алерте не внутренности библиотеки, а точное место поломки.

### errwrapMiddleware: %w сохраняет канон ошибок

```go
func errwrapMiddleware(method string) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                        resp, err := next(ctx, request)
                        if err != nil {
                                return nil, fmt.Errorf("%s: %w", method, err)
                        }
                        return resp, nil
                    }
        }
}

// errors.Is(err, kitratelimit.ErrLimited) по-прежнему true —
// %w сохраняет цепочку обёрток для errors.Is и errors.As.
```

Ключ к совместимости — глагол %w: внутри err хранится исходная ошибка, поэтому errors.Is из уроков 09-10 и encodeError из урока 05 находят канонический sentinel без изменений. Без %w (просто %v) цепочка рвётся, лимитный отказ доезжает до транспорта уже нераспознанным и уезжает в 500 — тихая поломка, которую ищут неделю. Обёртка стоит снаружи канонических проверок — например, сразу за loggingMiddleware — так, чтобы префикс попадал в лог, но не мешал breaker'у: gobreaker сравнивает по IsSuccessful и тоже умеет разворачивать обёртки.

### Где окажется префикс, а где sentinel

**Вопрос:** errwrapMiddleware обернула endpoint лимитером внутри. Клиент превысил лимит. Что увидит encodeError на транспорте, если обёртка использовала %w?

- Только префикс: uppercase: — sentinel потерян, ответ 500
- ✅ Обёрнутую ошибку: errors.Is найдёт ErrLimited и вернёт 429
- Обе ошибки, но errors.Is работает только без %w
- nil, потому что обёртка съедает ErrLimited

fmt.Errorf с %w создаёт обёртку, внутри которой лежит исходная ошибка: errors.Is(err, ErrLimited) разворачивает цепочку и находит sentinel, поэтому encodeError из урока 05 вернёт честный 429. Префикс при этом виден в строке ошибки и в логах — ровно то, для чего обёртка и ставилась.

### Сборка цепочки: канон stringsvc с защитным хвостом

Теперь собери всё. Пять обёрток из уроков 06-10 плюс две новые — полная цепочка endpoint-middleware stringsvc принимает вид: loggingMiddleware снаружи, затем NewErroringLimiter, затем kitot.TraceServer, затем instrumentingMiddleware, затем kitcb.Gobreaker, затем timeoutMiddleware, затем recoveryMiddleware — и только потом сам endpoint от [makeUppercaseEndpoint из урока 02](/courses/go-kit/lessons/02-endpoint/). Правила порядка, выведенные по ходу курса, сходятся в одно: дешевле отказ — раньше обёртка (лог до лимитера), наблюдаемость — снаружи защитного ядра (метрики и спаны видят и отказы breaker'а), защита зависимости — внутри (breaker ослышит таймауты и паники), и только endpoint — в самом центре. Fallback из урока 10, если он есть, надевается поверх breaker'а — он ловит именно его отказы. Ниже — код сборки двумя способами: построчным, как в уроках 08-10, и через endpoint.Chain. Оба дают идентичную цепочку; построчный удобнее для поурочного чтения, Chain — для конфигурируемости, когда список обёрток собирается из флагов запуска.

### Полная цепочка в main

```go
upper := makeUppercaseEndpoint(svc) // endpoint из урока 02

// Построчная сборка — читается как путь запроса сверху вниз:
upper = loggingMiddleware(logger)(upper)          // урок 06
upper = kitratelimit.NewErroringLimiter(allow)(upper) // урок 09
upper = kitot.TraceServer(tracer, "uppercase")(upper) // урок 08
upper = instrumentingMiddleware(count, hist)(upper)   // урок 07
upper = kitcb.Gobreaker(breaker)(upper)           // урок 10
upper = timeoutMiddleware(2 * time.Second)(upper) // новая
upper = recoveryMiddleware(logger)(upper)         // новая

// То же одним вызовом — первый аргумент снаружи:
upper = endpoint.Chain(
        loggingMiddleware(logger),
        kitratelimit.NewErroringLimiter(allow),
        kitot.TraceServer(tracer, "uppercase"),
        instrumentingMiddleware(count, hist),
        kitcb.Gobreaker(breaker),
        timeoutMiddleware(2*time.Second),
        recoveryMiddleware(logger),
)(upper)
```

Заметь: endpoint'ы строятся из уже обёрнутого в service-матрёшку сервиса — svc сюда приходит декорированным из следующего урока. Комментарии справа — не украшение, а живая документация порядка: когда через полгода появится восьмая обёртка, именно по этому списку ты решишь, куда её воткнуть. Переменные count, hist, breaker, allow объявлялись в уроках 07, 09 и 10 — здесь только сборка.

### Порядок защиты

**Вопрос:** В цепочке ... → kitcb.Gobreaker → timeoutMiddleware → recoveryMiddleware → endpoint соседний сервис завис намертво. Что откроет предохранитель?

- Ничего: timeout стоит внутри breaker'а и глушит ошибки
- ✅ Да: DeadlineExceeded вернётся из timeout и попадёт в Counts breaker'а
- Нет: только recovery сможет остановить трафик
- Breaker откроется, только если таймаут поставить снаружи

При дефолтном IsSuccessful дедлайны считаются неудачами зависимости — таймауты копятся в Counts, ReadyToTrip открывает цепь, и соседние вызовы перестают ждать по две секунды, а отбиваются мгновенно. Именно поэтому timeout стоит ВНУТРИ breaker'а: снаружи он бы гасил дедлайны до предохранителя и ослеплял его.

### Chain, Middlewares и ручная обёртка: когда что брать

Три способа композиции из урока 02 в большом сервисе перестают быть вопросом вкуса — у каждого появляется своя ниша. Ручная обёртка ep = mw(ep) — выбор для одной-двух обёрток или для мест, где порядок надо подсветить комментариями, как в блоке выше. endpoint.Chain — компактная запись фиксированного канона: все семь обёрток stringsvc собираются одним вызовом, и семантика «первый аргумент снаружи» читается как путь запроса — вызовы идут снаружи внутрь, ответы в обратную сторону. Срез endpoint.Middlewares — единственный способ, где состав определяется в рантайме: срез собирается по флагам запуска или конфигу (нет Jaeger — нет трейсера; режим разработки — без лимитера), после чего применяется одним вызовом метода Endpoint. Семантика у Chain и Middlewares одинаковая — первый элемент снаружи; внутри пакета Chain просто разворачивает аргументы в срез и обходит с конца. Практическое правило: канон сервиса держи в одном месте — срезе переменной middlewares — и применяй ко всем endpoint'ам; тогда добавление восьмой обёртки меняет одну строку, а не семь разных мест, и порядок не расползается между методами.

### Каталог обёрток stringsvc

| Обёртка | Откуда | Что делает | Место в цепочке |
| --- | --- | --- | --- |
| loggingMiddleware | урок 06 | пишет method/err/took через defer | снаружи, первая |
| NewErroringLimiter | урок 09 | токен из бакета или ErrLimited | вторая, до дорогих слоёв |
| kitot.TraceServer | урок 08 | спан операции с Failer-тегом | третья |
| instrumentingMiddleware | урок 07 | request_count и латентность | четвёртая |
| kitcb.Gobreaker | урок 10 | цепь closed/open/half-open | пятая, ядро защиты |
| timeoutMiddleware | этот урок | дедлайн context.WithTimeout | шестая, внутри breaker'а |
| recoveryMiddleware | этот урок | recover(): паника → err | седьмая, вокруг endpoint |
| fallback | урок 10 | деградированный ответ | поверх breaker'а |

### Паника сквозь цепочку

```go
upper = loggingMiddleware(logger)(upper)      // снаружи
upper = kitcb.Gobreaker(breaker)(upper)       // посередине
upper = recoveryMiddleware(logger)(upper)     // внутри
upper = panickingEndpoint                      // паникует всегда

resp, err := upper(ctx, UppercaseRequest{S: "go"})
```

**Что выведет программа?**

- Соединение оборвано, лог пуст, breaker не заметил
- ✅ err = internal panic, лог записал err, breaker засчитал неудачу
- resp = nil, err = nil — recover съел всё молча
- Паника дошла до loggingMiddleware и убила сервис

recovery стоит самой внутренней: паника ловится на замыкании, превращается в err и течёт вверх по обычному каналу — лог снаружи записывает err и took, breaker видит ошибку и копит неудачи, пока ReadyToTrip не откроет цепь. Вариант 1 случился бы без recovery: паника ушла бы в каркас, минуя все обёртки, и предохранитель остался бы слепым.

### Endpoint middleware против service middleware: граница

У тебя уже два этажа обёрток, и границу между ними пора провести словами, а не интуицией. Endpoint-обёртка видит ctx и interface{}: её стихия — путь запроса, дедлайны, наблюдаемость, лимиты — всё то, что не знает о домене. Service-декоратор из [урока 03](/courses/go-kit/lessons/03-service/) видит доменные аргументы: строку s, число символов, sentinel ErrEmpty; его стихия — кэш, бизнес-метрики, ретраи к хранилищу, логи уровня бизнеса. Проверочный вопрос для новой обёртки: нужны ли ей поля request-структуры? Если лог хочется писать в виде «в Uppercase передали пустую строку» — это декоратор; если «method=uppercase err=took=» — это endpoint-middleware. Второй вопрос: меняется ли семантика вызова? Лимитер и breaker меняют только доступ к вызову — endpoint-этаж; кэш и прокси из [урока 11](/courses/go-kit/lessons/11-lb-proxy/) меняют саму суть вызова, отвечая вместо вычисления — service-этаж. Обёртки endpoint и декораторы собираются в main до создания endpoint'ов: сначала матрёшка сервиса, потом endpoint'ы из неё, потом endpoint-цепочка. Следующий урок — весь про второй этаж: type ServiceMiddleware, декораторы с встраиванием и инъекция зависимостей.

### Чек-лист: пишешь свою обёртку

- Сигнатура — endpoint.Middleware: фабрика с зависимостями возвращает func(next) endpoint.Endpoint.
- Дорогое — на уровне фабрики и обёртки; на горячем пути только вызов next и defer.
- Именованные результаты и defer — если надо видеть ответ после next.
- Состояние — под мьютексом или атомарно; никаких мап без блокировки.
- Ошибки — sentinel или тип-носитель из урока 05; обёртка ошибок — только через %w.
- Позиция в цепочке — по правилам: наблюдаемость снаружи, защита внутри; recovery и timeout — в защитном хвосте.
- Тест — обёртка тестируется с fake-next из следующего урока 16, без транспорта.

### Сколько раз что исполняется

**Вопрос:** N запросов прошли через ep = myMiddleware(cfg)(ep). Сколько раз исполнилось тело фабрики myMiddleware и тело замыкания с вызовом next?

- Фабрика N раз, замыкание N раз
- ✅ Фабрика 1 раз, замыкание N раз
- Фабрика N раз, замыкание 1 раз
- Оба по одному разу

Фабрика и обёртка исполняются один раз при сборке в main — их тело не знает про запросы и не должно ничего измерять. Замыкание — это и есть обёрнутый endpoint: оно исполняется на каждый запрос. Отсюда правило размещения: конфиг и соединения — на фабрике, пер-запросная работа — в замыкании.

### Восстанови защитный хвост

Собери timeout- и recovery-обёртки: пропущены три вызова, без которых обёртки не работают.

```go
func timeoutMiddleware(d time.Duration) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (interface{}, error) {
                        ctx, cancel := context.___(ctx, d)
                        defer ___()
                        return next(ctx, request)
                }
        }
}

func recoveryMiddleware(logger log.Logger) endpoint.Middleware {
        return func(next endpoint.Endpoint) endpoint.Endpoint {
                return func(ctx context.Context, request interface{}) (resp interface{}, err error) {
                        defer func() {
                                if r := ___(); r != nil {
                                        err = fmt.Errorf("internal panic: %v", r)
                                }
                        }()
                        return next(ctx, request)
                }
        }
}
```

Подсказка: Первый пропуск — производный контекст с дедлайном; второй — освобождение ресурсов таймера; третий — функция, возвращающая значение паники только внутри defer.

Правильные ответы в пропусках: `WithTimeout`, `cancel`, `recover`

### Путь запроса через полную цепочку

Расставь этапы вызова Uppercase по слоям обёрток stringsvc.

Правильный порядок строк:

1. `Декодер HTTP собрал UppercaseRequest из тела запроса`
2. `loggingMiddleware засёк begin и передал вызов дальше`
3. `NewErroringLimiter взял токен из бакета — или вернул ErrLimited`
4. `kitot.TraceServer открыл спан uppercase`
5. `instrumentingMiddleware отметил старт замера латентности`
6. `kitcb.Gobreaker пропустил вызов через замкнутую цепь`
7. `timeoutMiddleware поставил дедлайн на производный контекст`
8. `recoveryMiddleware вошёл в замыкание с defer recover`
9. `endpoint вызвал StringService и вернул UppercaseResponse`

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

- **endpoint.Middleware** — type Middleware func(Endpoint) Endpoint: фабрика принимает зависимости, возвращает обёртку next → новый endpoint; исполняется один раз при сборке.
- **Три уровня вложенности** — Фабрика → обёртка (next) → замыкание (ctx, request): первый и второй уровни — один раз при сборке, третий — на каждый запрос.
- **recoveryMiddleware** — defer + recover() в замыкании с именованными результатами: паника превращается в err; ставится самой внутренней — breaker и логи видят честную ошибку.
- **timeoutMiddleware** — context.WithTimeout(ctx, d) + defer cancel: дедлайн на операцию; сработавший таймаут должен попадать в breaker как неудача — обёртка внутри breaker'а.
- **Канон порядка stringsvc** — logging → лимитер → трейсер → метрики → breaker → timeout → recovery → endpoint: дешёвый отказ раньше, наблюдаемость снаружи, защита внутри.
- **%w в обёртке ошибок** — fmt.Errorf("%s: %w", method, err) сохраняет sentinel: errors.Is и errors.As находят каноническую ошибку, encodeError вернёт честный статус.

> **Ловушка: обёртка на горячем пути.** Обёртка применяется к endpoint один раз при сборке — если она оказалась внутри замыкания, обрабатывающего запрос (например, ты обернул endpoint в цикле декодера), каждый запрос наденет новую копию обёртки: память утекает, метрики множатся, порядок цепочки ломается непредсказуемо. Оборачивание — это действие func main, и код-ревью должно ловить любой вызов middleware за пределами сборки.

### Что дальше

Endpoint-этаж готов целиком: семь обёрток, канон порядка, три правила размещения и умение писать свои за десять минут. Остался второй этаж. Урок 15-service-middleware — про service-декораторы: type ServiceMiddleware func(StringService) StringService, встраивание интерфейса, кэш и ретраи как обёртки домена, композиция матрёшкой и инъекция зависимостей; прокси из [урока 11-lb-proxy](/courses/go-kit/lessons/11-lb-proxy/) встанет там на своё законное место. После него — [тестирование go-kit](/courses/go-kit/lessons/16-testirovanie/) в уроке 16: тесты endpoint'ов, fake-next для обёрток и httptest для транспорта. Карта курса — на [странице курса](/courses/go-kit/).

> **Практика локально.** Урок требует установленного Go и модуля go-kit: в песочнице курса сторонние пакеты не исполняются, поэтому интерактивы построены на чтении кода. Локально: собери цепочку из блока выше, добавь в endpoint тестовую панику через переменную окружения и убедись, что лог записывает err=internal panic, /metrics показывает error=true, а breaker через ReadyToTrip открывает цепь; затем убери defer cancel() из timeout-обёртки и посмотри рост горутин под нагрузкой.

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

- Endpoint middleware — func(endpoint.Endpoint) endpoint.Endpoint: фабрика и обёртка исполняются один раз при сборке, замыкание — на каждый запрос.
- Шесть правил обёртки: не менять типы, одна ответственность, named returns + defer, потокобезопасное состояние, каноничные ошибки, уважение к контексту.
- recoveryMiddleware ловит панику через defer/recover в именованный err — логи, метрики и breaker видят честную ошибку вместо обрыва соединения.
- timeoutMiddleware ставит дедлайн через context.WithTimeout и defer cancel; таймаут должен быть виден breaker'у, поэтому обёртка живёт внутри него.
- Обёртка ошибок через fmt.Errorf с %w сохраняет sentinel: errors.Is находит ErrLimited и ErrEmpty, encodeError возвращает честный статус.
- Канон цепочки stringsvc: logging → лимитер → трейсер → метрики → breaker → timeout → recovery → endpoint; Chain и Middlewares дают ту же семантику — первый элемент снаружи.
- Граница этажей: endpoint-обёртка про путь запроса и наблюдаемость, service-декоратор — про домен; вопрос «нужны ли поля request-структуры» решает выбор.

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

Собери защитный хвост stringsvc своими руками. Первое: напиши recoveryMiddleware по уроку, добавь в endpoint тестовую панику через переменную окружения PANIC_TEST и проверь локально три вещи — лог из урока 06 содержит err=internal panic и стек, /metrics из урока 07 инкрементирует request_count с error=true, breaker из урока 10 открывает цепь после порога ReadyToTrip. Второе: добавь timeoutMiddleware с дедлайном в сто миллисекунд и зависимый endpoint с Sleep в секунду; убедись, что вернулся context.DeadlineExceeded, breaker посчитал неудачу, а после убирания defer cancel() рост горутин виден в pprof. Третье: напиши errwrapMiddleware с %w, поставь её поверх лимитера и проверь, что errors.Is(err, ErrLimited) остаётся true после обёртки, а encodeError отдаёт 429. Четвёртое: перепиши построчную сборку на endpoint.Chain и endpoint.Middlewares со срезом из конфига — без Jaeger трейсер не добавляется; запиши, как выглядит итоговый срез. Пятое: по чек-листу обёрток проверь свою цепочку и запиши одно место, где порядок пришлось бы поменять, если бы метрики стояли внутри breaker'а.

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

### Почему recovery не ставят первым — ведь тогда он поймает панику из любой обёртки?

Паники в обёртках — признак бага в их коде, и глотать их на входе опасно: сломанный middleware продолжит работать и маскировать проблему. Recovery на защите вызывает сам обёрнутый код — адаптер, сервис, библиотеку — и превращает в ошибку ровно ту панику, к которой стек готов. Если боишься паник в самих обёртках, тестируй их отдельно (урок 16) и держи каркасную страховку net/http или gRPC как последний рубеж.

### Timeout на endpoint или WriteTimeout на http.Server — что выбрать?

Это разные уровни одной задачи. WriteTimeout ограничивает всю обработку на каркасе — он не виден endpoint-слою, breaker'у и метрикам, зато покрывает и декодер, и энкодер. Timeout-обёртка ограничивает конкретную операцию, видна предохранителю и работает одинаково для HTTP и gRPC. Рабочая связка: WriteTimeout как внешний страховочный потолок с запасом, endpoint-дедлайн — как точный лимит операции, который попадает в breaker.

### Можно ли обёрткой изменить request-структуру — например, дополнить поле?

Технически можно, но не нужно: обёртка, которая знает тип request-а, привязывает себя к одному методу и ломает транспортную независимость — тот же код перестаёт работать на соседнем endpoint'е. Данные запроса достраиваются в декодере (RequestFunc на ServerBefore), контекстные значения — в ServerBefore или ctx-обёртке, а сервисные поля с нормальными типами — в адаптере из урока 02. Обёртки работают с ctx, err и поведением, а не с полями структур.
