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

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

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

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

HTTP-транспорт

Собрано ядро и домен — осталось выпустить stringsvc в сеть. Транспортный слой — единственное место курса, где легален *http.Request, и в go-kit http он сведён к трём функциям: конструктор kithttp NewServer оборачивает endpoint в http.Handler, декодер читает запрос, энкодер пишет ответ. Ты смонтируешь оба метода на стандартный mux, научишься отдавать честные статусы через Failer и поймёшь, почему сломанный JSON по умолчанию уезжает с 500 — и как это чинить. После урока сервис наконец отвечает curl'у.

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

Транспортный слой: три функции вместо фреймворка#

Вспомни луковицу из первого урока: транспорт — внешний слой, привязанный к протоколу. В пакете transport/http (в коде его обычно импортируют с алиасом kithttp) транспорт go kit сводится к трем сигнатурам. NewServer(endpoint, decode, encode, options...) принимает endpoint и две функции, возвращая Server — обычный http.Handler. DecodeRequestFunc — func(context.Context, http.Request) (interface{}, error): декодер превращает HTTP-запрос в request-структуру, которую ждёт адаптер. EncodeResponseFunc — func(context.Context, http.ResponseWriter, interface{}) error: энкодер сериализует response-структуру в ответ. Всё. Ни роутера, ни миддлварь-цепочки, ни магии привязок: транспорт — это три функции, и ровно они единственные в проекте знают про тело запроса и заголовки. Такой размен — многословность за явность — и есть фирменный стиль go kit http: замени gRPC — поменяются три функции, сервис и endpoint'ы останутся нетронутыми.

Монтируем stringsvc: NewServer на стандартном mux

GO
import (
        "log"
        "net/http"
        "os"

        "github.com/go-kit/log"      // kitlog
        "github.com/go-kit/kit/endpoint"
        kithttp "github.com/go-kit/kit/transport/http"
)

func main() {
        logger := kitlog.NewJSONLogger(os.Stdout)

        var svc StringService = stringService{}
        upper := makeUppercaseEndpoint(svc)
        count := makeCountEndpoint(svc)

        mux := http.NewServeMux()
        mux.Handle("/uppercase", kithttp.NewServer(
                upper,
                decodeUppercaseRequest, // *http.Request -> UppercaseRequest
                encodeResponse,         // response -> JSON
        ))
        mux.Handle("/count", kithttp.NewServer(
                count,
                decodeCountRequest,
                encodeResponse,
        ))

        logger.Log("msg", "listening", "addr", ":8080")
        log.Fatal(http.ListenAndServe(":8080", mux))
}

Каждый метод сервиса — свой NewServer со своим декодером, энкодер общий. kithttp.NewServer возвращает http.Handler, поэтому роутер остаётся обычным: стандартный mux, gorilla, chi — что нравится. Метод по умолчанию — POST, это учтём в curl-проверке; ограничение методов — забота роутера, не go-kit.

Как работает NewServer изнутри#

Заглянем в server.go пакета transport/http — там меньше сотни строк, и полезно понимать путь запроса. ServeHTTP берёт r.Context(), прогоняет через ServerBefore-функции (о них ниже), затем вызывает декодер: получил request — передал его в endpoint вместе с контекстом; ошибка декодера — сразу в error encoder, endpoint не зовётся. Дальше ответ попадает в энкодер, а его ошибка уходит в ServerErrorHandler — обычно в логгер, потому что заголовки уже уехали и клиенту сказать нечего. Четыре серверных опции дополняют картину: ServerBefore добавляет в контекст значения из запроса — путь, заголовки, auth; ServerAfter срабатывает после ответа — туда лягут метрики и трейсинг в уроках 07–08; ServerErrorEncoder подменяет то, как ошибка превращается в HTTP-ответ; ServerErrorHandler ловит ошибки энкодера. Правило чтения простое: NewServer — это конвейер декодер → endpoint → энкодер с четырьмя хуками по краям, и все хуки транспортные, к домену отношения не имеют.

Декодер: единственный, кто трогает тело#

Декодер получает *http.Request и возвращает request-структуру — вот и весь контракт. Что здесь легально: json.NewDecoder(r.Body).Decode(&req) для POST-тел, r.URL.Query() для GET-параметров, переменные маршрута, если роутер их умеет, заголовки — например, Authorization, который потом уйдёт в контекст. Три практики, которые сэкономят нервы. Первая: тело читается один раз — декодер обязан, а не «может» прочитать r.Body до конца, иначе keep-alive-соединения начнут гнить; json.NewDecoder делает это сама. Вторая: возвращай примитивные структуры ровно того типа, что ждёт адаптер из урока 02 — любой mismatch выстрелит паникой assertion, и это будет твоя вина, не go-kit. Третья: ошибки декодера — это транспортный сбой, они идут по error-каналу и по умолчанию превращаются в 500; кастомизируем это на следующем уроке кодеков. Для GET-эндпоинтов тело пустое — декодер просто собирает структуру из query, и это нормально: декодер — не ритуал JSON, а переводчик HTTP в домен.

Декодеры для Uppercase и Count

GO
import (
        "context"
        "encoding/json"
        "net/http"
)

// POST /uppercase {"s":"hello"} — читаем тело.
func decodeUppercaseRequest(_ context.Context, r *http.Request) (interface{}, error) {
        var req UppercaseRequest
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
                return nil, err // транспортная ошибка: endpoint не вызовется
        }
        return req, nil
}

// GET /count?s=hello — тело пустое, берём query.
func decodeCountRequest(_ context.Context, r *http.Request) (interface{}, error) {
        s := r.URL.Query().Get("s")
        return CountRequest{S: s}, nil
}

Две типовые формы декодера. Uppercase читает JSON-тело; ошибка Decode возвращается наружу — она инфраструктурная. Count собирает запрос из query: декодер не обязан трогать тело. Контекст не используется и назван _ — значения из заголовков сюда добавят ServerBefore-хуки в уроках трейсинга и аутентификации.

Энкодер: сериализация и Content-Type#

Энкодер симметричен: получает response-структуру и пишет её в http.ResponseWriter. Базовая форма — три строки: выставить Content-Type: application/json; charset=utf-8 и вернуть json.NewEncoder(w).Encode(response). Заученная ловушка HTTP здесь прячется в порядке операций: первый же вызов, который пишет в тело, триггерит неявный WriteHeader(200) — после этого менять заголовки бессмысленно, они уже отправлены. Поэтому Set — всегда до Encode, а явный WriteHeader — только если хочешь статус отличный от 200. Ошибка, которую возвращает энкодер, — отдельная история: когда Encode падает (клиент отвалился, срез по конвейеру), заголовки уже ушли, сообщить клиенту нечего — ошибка попадает в ServerErrorHandler и логируется. Статус по умолчанию 200 устраивает счастливый путь; несчастливый — следующий раздел, и там появится таблица codeFrom.

Энкодер и честные статусы: Failer в действии

GO
import (
        "context"
        "encoding/json"
        "net/http"

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

// Счастливый путь: JSON со статусом 200.
func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error {
        if f, ok := response.(endpoint.Failer); ok && f.Failed() != nil {
                // Бизнес-ошибка: не 500, а честный статус.
                encodeError(ctx, f.Failed(), w)
                return nil
        }
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        return json.NewEncoder(w).Encode(response)
}

func encodeError(_ context.Context, err error, w http.ResponseWriter) {
        w.Header().Set("Content-Type", "application/json; charset=utf-8")
        w.WriteHeader(codeFrom(err))
        json.NewEncoder(w).Encode(map[string]interface{}{"error": err.Error()})
}

func codeFrom(err error) int {
        switch err {
        case ErrEmpty:
                return http.StatusBadRequest // 400
        default:
                return http.StatusInternalServerError // 500
        }
}

Канон stringsvc в редакции Failer из урока 02: response проверяется на Failed(), и бизнес-ошибка кодируется отдельно — со статусом из codeFrom, а не пачкой 500. ErrEmpty стал честным 400. Энкодер — последний пункт, где допустимы заголовки и статусы; сервис про HTTP не узнает никогда.

Ошибки транспорта: три пути к клиенту#

Сведём всё, что знает транспорт go kit об ошибках, — тут три принципиально разных маршрута. Первый: ошибка декодера — сломанный JSON, недостающий параметр; она возвращается из DecodeRequestFunc, endpoint не вызывается, а ответ формирует error encoder. По умолчанию работает DefaultErrorEncoder: статус 500 и текст ошибки в text/plain — нечестно для плохого запроса, поэтому следующий урок посвятим собственному ErrorEncoder через опцию ServerErrorEncoder. Второй: бизнес-ошибка из response-структуры — Failed() != nil; её ловит наш encodeResponse и кодирует через encodeError со статусом из codeFrom. Третий: err endpoint'а — инфраструктурный сбой; он тоже уходит в error encoder, и это правильно: база недоступна — клиенту честный 500, а breaker'у на клиентской стороне — сигнал. Сравни с gin, где c.JSON и c.Abort размазаны по хендлерам: здесь кодирование ошибки — функция с таблицей статусов, единая для всех эндпоинтов. Такая дисциплина делает поведение API предсказуемым: одинаковые ошибки — одинаковые ответы на любом транспорте.

Куда уходит ошибка: путь к статусу#

ОшибкаПримерКто ловитЧто получит клиент
Бизнес, известнаяErrEmpty из сервисаencodeResponse через Failer400 и JSON с полем error
Бизнес, не смапленнаяновый sentinel без ветки в codeFromencodeError → default500 — добавь ветку в codeFrom
Сломанный запросEOF от json.DecoderDefaultErrorEncoder500 и text/plain — лечится ErrorEncoder (урок 05)
Инфраструктурный сбойбаза недоступна: err endpoint'аDefaultErrorEncoder500 — честный сигнал breaker'у
Ошибка энкодераEncode упал на отвалившемся клиентеServerErrorHandler → логобрубленный ответ — клиенту нечего сказать
Паникапрограммистская ошибкапроцесс падает громкосоединение рвётся — канон урока 01

Роутер остаётся твоим#

NewServer возвращает http.Handler — значит, go-kit не отбирает маршрутизацию, и это решение FAQ, а не случайность. Стандартный http.NewServeMux тянет простые проекты; gorilla/mux и chi добавляют переменные пути и middleware уровня роутера; gin можно смонтировать обёрткой его хендлера, если команда уже на нём живёт. Два практических замечания. Методы: mux.Handle не ограничивает метод — POST/GET различаются на уровне роутера (gorilla: r.Methods("POST")), иначе декодер JSON встретит GET с пустым телом и честно вернёт ошибку декодера. Переменные пути: если нужен /profiles/{id}, бери роутер с параметрами и передавай их в request-структуру из декодера — домен и endpoint по-прежнему не видят URL. Пара NewServer + чужой роутер — типовой гибрид больших проектов: go-kit отвечает за слои и обёртки, роутер — за маршруты. В уроке 13 тот же endpoint посадим на gRPC, и станет видно, зачем всё это разделение.

ServerBefore и ServerAfter: хуки контекста#

Осталось два хука, на которые в этом уроке только смотрим. ServerBefore принимает RequestFunc — функцию func(ctx context.Context, r *http.Request) context.Context — и вызывается до декодера: здесь берут из запроса то, что относится к условиям вызова, а не к данным — заголовок Authorization, X-Request-ID, путь. Значение кладётся в контекст с ключом и едет вниз через endpoint в сервис. ServerAfter — зеркальный хук после ответа: func(ctx, response) — туда ложатся метрики и трейсы. Зачем это знать сейчас: чтобы не соблазниться положить данные запроса в декодере в ctx — данные едут в request-структуре, условия вызова — в контексте через ServerBefore. Практика появится по делу: идентификатор запроса — в уроке 06 про логи, спаны трейсинга — в 08, JWT — в 23. Сегодня достаточно прочитать опции в сигнатуре NewServer и не бояться их.

Чеклист транспортного слоя#

  1. На каждый endpoint — свой kithttp.NewServer; декодер уникален, энкодер общий.
  2. Декодер — единственный читатель r.Body, query и заголовков; ошибки декодера — транспортные.
  3. Content-Type и статусы выставляются только в энкодере и encodeError, до первого Write.
  4. Бизнес-ошибки кодируются через Failer и таблицу codeFrom, а не все подряд 500.
  5. Роутер внешний: методы и переменные пути — его забота; NewServer отдаёт http.Handler.
  6. Ни одна транспортная сущность не проходит дальше декодера: *http.Request в endpoint не везут.

Что возвращает kithttp.NewServer и куда его монтировать?

Сервис вернул ErrEmpty, response-структура реализует Failer, encodeResponse написан по канону. Какой статус увидит клиент?

Где выставляется Content-Type: application/json в go-kit сервисе?

Сломанный JSON: какой статус по умолчанию?

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

// POST /uppercase телом "not json"
// Декодер: json.NewDecoder(r.Body).Decode(&req)
// ServerErrorEncoder НЕ настроен

Переставили Set и Encode: дойдёт ли Content-Type?

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

func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error {
        json.NewEncoder(w).Encode(response) // сначала тело
        w.Header().Set("Content-Type", "application/json; charset=utf-8") // потом заголовок
        return nil
}

Восстанови монтаж эндпоинта и энкодер

Собери транспортный слой. Пропуски — по порядку: конструктор из kithttp, метод заголовка, метод энкодера.

mux.Handle("/uppercase", kithttp.( upper, decodeUppercaseRequest, encodeResponse, )) func encodeResponse(_ context.Context, w http.ResponseWriter, response interface{}) error { w.Header().("Content-Type", "application/json; charset=utf-8") return json.NewEncoder(w).(response) }

Путь запроса через транспорт

Расставь этапы пути POST /uppercase в правильном порядке: от ListenAndServe до отправленного тела.

Строки

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

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

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

Термин

kithttp.NewServer

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

Конструктор transport/http: endpoint + декодер + энкодер + опции → *Server, обычный http.Handler для любого роутера.

1 / 6

Что дальше#

stringsvc отвечает по сети: POST /uppercase и GET /count работают, статусы честные, слои не слиплись. Но дефолты транспорта видны: плохой JSON уезжает с 500, а кодирование ошибок умещается в один case. Следующий шаг — урок 05-codeki: свой ErrorEncoder, аккуратные JSON-ошибки и правила кодеков. Освежить Basis можно в уроке 02-endpoint — теперь ты знаешь, кто вызывает адаптеры, — а карта курса лежит в уроке 01-filosofiya и на странице курса. Транспорт был последним недостающим слоем — дальше курс надевает на них наблюдаемость.

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

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

Вывод

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

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

  • Транспорт — три функции: NewServer(endpoint, decode, encode, options...) возвращает http.Handler.
  • Декодер единственный трогает тело, query и заголовки; его ошибка — транспортная, endpoint не зовётся.
  • Энкодер пишет Content-Type до первого Write: неявный WriteHeader(200) срабатывает на первой записи тела.
  • Бизнес-ошибки кодируются через Failer и encodeError с таблицей codeFrom: ErrEmpty — 400, не 500.
  • Дефолтный error encoder отдаёт 500 text/plain на всё — включая сломанный JSON; свой ErrorEncoder — урок 05.
  • Роутер остаётся твоим: mux, gorilla, chi, gin; методы и переменные пути — его забота.
  • ServerBefore кладёт условия вызова в ctx до декодера; ServerAfter работает после ответа — территория метрик и трейсинга.

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

Доведи stringsvc до работающего сервиса: смонтируй оба endpoint'а через kithttp.NewServer на стандартном mux, подключи декодеры из урока и общий encodeResponse с encodeError и codeFrom. Проверь curl'ом три сценария и запиши наблюдения: валидный POST /uppercase с телом {"s":"hi"}, GET /count?s=hello через query и POST со сломанным JSON — на последнем поймай дефолтный 500 от DefaultErrorEncoder и объясни себе, почему это нечестно. Затем добавь в codeFrom вторую sentinel-ошибку своего сервиса из домашки урока 03 и убедись, что статус поменялся без правки сервиса. В конце попробуй смонтировать один endpoint на gin или chi — NewServer должен вписаться без изменений в декодерах и энкодерах.

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

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

Как смонтировать go-kit endpoint на gin или chi?

NewServer возвращает http.Handler, поэтому chi монтирует напрямую: r.Post("/uppercase", handler). Для gin нужна обёртка его хендлера вокруг handler.ServeHTTP — стандартный приём. Роутер отвечает за методы и параметры пути, go-kit — за слои и декодеры.

GET с query или POST с JSON — как правильно?

Оба легальны, решает декодер: для GET он собирает request-структуру из r.URL.Query(), для POST — из тела. Разница только в форме декодера; endpoint, сервис и энкодер не меняются. Метод ограничивает роутер, не go-kit.

Почему сломанный JSON даёт 500, а не 400?

Потому что ошибка декодера попадает в DefaultErrorEncoder, который отвечает 500 и text/plain на любую ошибку. Это дефолт, а не приговор: настроенный ServerErrorEncoder с таблицей статусов сведёт ошибки декодирования к 400 — именно этим займётся урок 05 про кодеки.

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

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

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

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

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

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

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