Содержание урока · 16 разделов
- Пять обёрток уже есть: что осталось разобрать
- Анатомия обёртки: три уровня вложенности функций
- Шесть правил хорошей обёртки
- Своя обёртка №1: recovery — паника становится ошибкой
- Куда встаёт recovery: самая внутренняя позиция
- Дедлайн в err: что видят остальные слои
- Своя обёртка №3: обогащение ошибок именем метода
- Сборка цепочки: канон stringsvc с защитным хвостом
- Chain, Middlewares и ручная обёртка: когда что брать
- Каталог обёрток stringsvc
- Endpoint middleware против service middleware: граница
- Чек-лист: пишешь свою обёртку
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
Пять обёрток уже есть: что осталось разобрать#
Оглянись назад: loggingMiddleware из урока 06, instrumentingMiddleware из урока 07, kitot.TraceServer из урока 08, NewErroringLimiter из урока 09 и kitcb.Gobreaker из урока 10 — пять работающих обёрток endpoint, и каждая собрана по одному лекалу. Урок 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 обёртки этого сорта — логи, метрики, лимиты — различаются только телом замыкания.
Скелет обёртки: от фабрики до замыкания
GOimport (
"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 паниковать не должен, но чужой код в адаптере или сторонней библиотеке — может. И одно место вместо трёх: HTTP-транспорт и gRPC-транспорт получают защиту одновременно, писать по копии на транспорт не нужно — транспортная независимость, обещанная в уроке 01.
recoveryMiddleware: defer + recover на замыкании
GOimport (
"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
GOfunc 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-обёртка — это страховка на уровне операции, транспортная независимость в чистом виде.
Тонкий случай дедлайна
Прочитайте код и выберите, что он выведет:
ep := timeoutMiddleware(100 * time.Millisecond)(base)
// base внутри делает time.Sleep(1 * time.Second) и возвращает ("ok", nil)
resp, err := ep(context.Background(), UppercaseRequest{S: "go"})Своя обёртка №3: обогащение ошибок именем метода#
Третья обёртка демонстрирует, что middleware может менять не только поведение, но и саму ошибку, не трогая response-структуры. Проблема: после логов, метрик и breaker'а в err может прилететь голая «connection refused» без намёка, какой метод сервиса пострадал; по логам Uppercase и Count различить несложно, но в системе с двадцатью endpoint'ами контекст ошибки стоит того, чтобы добавить его централизованно. Решение — обёртка, которая оборачивает ошибку через fmt.Errorf с глаголом %w: каноническая ошибка остаётся извлекаемой через errors.Is (лимитер, breaker и encodeError из уроков 09-10 продолжают работать), а поверх появляется префикс с именем метода. Этот приём — стандартная идиома Go, и в endpoint-мире он особенно удобен: одна обёртка на все методы вместо правки каждого адаптера. Заодно она показывает паттерн «обёртка-трансформер»: next вызывается, результат проверяется, при необходимости — модифицируется и возвращается дальше. Такие обёртки пишутся минут за десять, а экономят часы дежурств: дежурный видит в алерте не внутренности библиотеки, а точное место поломки.
errwrapMiddleware: %w сохраняет канон ошибок
GOfunc 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?
Сборка цепочки: канон stringsvc с защитным хвостом#
Теперь собери всё. Пять обёрток из уроков 06-10 плюс две новые — полная цепочка endpoint-middleware stringsvc принимает вид: loggingMiddleware снаружи, затем NewErroringLimiter, затем kitot.TraceServer, затем instrumentingMiddleware, затем kitcb.Gobreaker, затем timeoutMiddleware, затем recoveryMiddleware — и только потом сам endpoint от makeUppercaseEndpoint из урока 02. Правила порядка, выведенные по ходу курса, сходятся в одно: дешевле отказ — раньше обёртка (лог до лимитера), наблюдаемость — снаружи защитного ядра (метрики и спаны видят и отказы breaker'а), защита зависимости — внутри (breaker ослышит таймауты и паники), и только endpoint — в самом центре. Fallback из урока 10, если он есть, надевается поверх breaker'а — он ловит именно его отказы. Ниже — код сборки двумя способами: построчным, как в уроках 08-10, и через endpoint.Chain. Оба дают идентичную цепочку; построчный удобнее для поурочного чтения, Chain — для конфигурируемости, когда список обёрток собирается из флагов запуска.
Полная цепочка в main
GOupper := 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 соседний сервис завис намертво. Что откроет предохранитель?
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'а |
Паника сквозь цепочку
Прочитайте код и выберите, что он выведет:
upper = loggingMiddleware(logger)(upper) // снаружи
upper = kitcb.Gobreaker(breaker)(upper) // посередине
upper = recoveryMiddleware(logger)(upper) // внутри
upper = panickingEndpoint // паникует всегда
resp, err := upper(ctx, UppercaseRequest{S: "go"})Endpoint middleware против service middleware: граница#
У тебя уже два этажа обёрток, и границу между ними пора провести словами, а не интуицией. Endpoint-обёртка видит ctx и interface{}: её стихия — путь запроса, дедлайны, наблюдаемость, лимиты — всё то, что не знает о домене. Service-декоратор из урока 03 видит доменные аргументы: строку s, число символов, sentinel ErrEmpty; его стихия — кэш, бизнес-метрики, ретраи к хранилищу, логи уровня бизнеса. Проверочный вопрос для новой обёртки: нужны ли ей поля request-структуры? Если лог хочется писать в виде «в Uppercase передали пустую строку» — это декоратор; если «method=uppercase err=took=» — это endpoint-middleware. Второй вопрос: меняется ли семантика вызова? Лимитер и breaker меняют только доступ к вызову — endpoint-этаж; кэш и прокси из урока 11 меняют саму суть вызова, отвечая вместо вычисления — 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?
Восстанови защитный хвост
Собери timeout- и recovery-обёртки: пропущены три вызова, без которых обёртки не работают.
Путь запроса через полную цепочку
Расставь этапы вызова Uppercase по слоям обёрток stringsvc.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Термины урока
Обёртка применяется к endpoint один раз при сборке — если она оказалась внутри замыкания, обрабатывающего запрос (например, ты обернул endpoint в цикле декодера), каждый запрос наденет новую копию обёртки: память утекает, метрики множатся, порядок цепочки ломается непредсказуемо. Оборачивание — это действие func main, и код-ревью должно ловить любой вызов middleware за пределами сборки.
Что дальше#
Endpoint-этаж готов целиком: семь обёрток, канон порядка, три правила размещения и умение писать свои за десять минут. Остался второй этаж. Урок 15-service-middleware — про service-декораторы: type ServiceMiddleware func(StringService) StringService, встраивание интерфейса, кэш и ретраи как обёртки домена, композиция матрёшкой и инъекция зависимостей; прокси из урока 11-lb-proxy встанет там на своё законное место. После него — тестирование go-kit в уроке 16: тесты endpoint'ов, fake-next для обёрток и httptest для транспорта. Карта курса — на странице курса.
Урок требует установленного Go и модуля go-kit: в песочнице курса сторонние пакеты не исполняются, поэтому интерактивы построены на чтении кода. Локально: собери цепочку из блока выше, добавь в endpoint тестовую панику через переменную окружения и убедись, что лог записывает err=internal panic, /metrics показывает error=true, а breaker через ReadyToTrip открывает цепь; затем убери defer cancel() из timeout-обёртки и посмотри рост горутин под нагрузкой.
Песочница урока
Редактируйте код и запускайте его прямо здесь — интерпретатор работает в браузере, ничего устанавливать не нужно. За каждый запуск — +2 XP.
Запустите код, чтобы увидеть вывод.
Коротко: что запомнить
- 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 и поведением, а не с полями структур.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
gRPC-Go: высокопроизводительный RPC
Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.
- Писать .proto-схемы и генерировать код
- Реализовывать unary и streaming сервисы
- Управлять метаданными, дедлайнами и отменами
- Строить interceptors для логов и аутентификации