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

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

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

Урок 17 из 25 ~20 мин +50 XP

Структура go-kit проекта

Шестнадцать уроков росли внутри одного main.go — и к уроку 13 он стал музеем всего курса: здесь и конфигурация трейсера, и сборка endpoint'ов, и два слушателя, и регистратор Consul. Пора разложить файлы по полкам. Тема урока — структура go kit проекта: куда положить сервис из урока 03, endpoint'ы из урока 02, транспорты из уроков 04 и 13, чтобы импорты текли в одну сторону, а main остался точкой сборки. Ты построишь канонический go kit project layout с каталогами cmd и internal, соберёшь Set-конструкторы для endpoint'ов и сервисных декораторов и увидишь, почему большой проект go kit масштабируется на команду без God-файла: компилятор сам сторожит границы слоёв, которые мы выстраивали шестнадцать уроков.

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

От God-файла к раскладке#

Открой main.go, который мы писали с урока 01-filosofiya: конфигурация Jaeger-трейсера из урока 08, сборка сервисных прокси из урока 11, консул-регистратор из урока 12, два слушателя из уроков 04 и 13 — сотни строк, каждая из которых отвечает за свою вселенную. Пока сервис один, это терпимо; но проект растёт: добавляется третий endpoint, четвёртая интеграция, второй разработчик — и God-файл становится узким местом команды, где каждый merge-конфликт приходится распутывать вручную. Тесту сервиса из урока 03 всё это не нужно, клиенту из урока 11 — тоже, а компилятор не мешает им перепутаться: ничто не запрещает вызвать consulapi.NewClient из середины бизнес-метода. Ответ — раскладка по каталогам, повторяющая слои курса. Важно честно сказать: go-kit структура не предписана — это библиотека, а не каркас, у неё нет генераторов и обязательных папок. Но слои, в которые мы вкладывались с первого урока, напрашиваются в каталоги сами: сервис отдельно, endpoint'ы отдельно, транспорты отдельно. Раскладка, которую ты соберёшь, подчиняется простому правилу: структура go kit проекта — производная слоёв, а не вкуса: если раскладка повторяет endpoint → service → transport, она почти наверняка правильная.

Каноническая раскладка: cmd, internal, pb#

Возьми за основу конвенцию, которую сообщество Go выработало задолго до go-kit и которую использует сам тулкит в своих примерах: go kit project layout начинается с двух каталогов верхнего уровня — cmd и internal. cmd/stringsvc/main.go — точка входа, и в ней ровно одна обязанность: собрать сервис из деталей и запустить. Всё остальное живёт в internal — магическом для новичка каталоге: пакет, объявленный внутри internal, может быть импортирован ТОЛЬКО кодом того же модуля; компилятор не пустит чужой проект к твоим внутренностям, и API-гарантии тебе обеспечены бесплатно. Внутри internal лежат пакеты по слоям: service, endpoint, transport, platform — по одному на слой курса. Чего в раскладке НЕТ, и это не случайность: папок models, utils, helpers, common. Пакет без внятной ответственности — это помойка, куда складывают всё, что не хотелось продумывать; через год utils разрастётся в графа зависимостей, который невозможно разорвать. Границу «свой-чужой» в go-kit ты уже проводил по канону урока 05: кодеки — единственное место, где легальны транспортные типы; теперь у этой границы появилось имя каталога.

Дерево проекта stringsvc

GO
stringsvc/
├── cmd/
│   └── stringsvc/
│       └── main.go           // композиционный корень: config, сборка, слушатели
├── internal/
│   ├── service/
│   │   ├── service.go        // интерфейс StringService, реализация, sentinel-ошибки
│   │   └── middleware.go     // ServiceMiddleware: прокси из урока 11, декораторы
│   ├── endpoint/
│   │   ├── endpoint.go       // request/response-пары, Failer, make*Endpoint, Set
│   │   └── middleware.go     // обёртки: логи, метрики, спаны, лимитер, breaker
│   ├── transport/
│   │   ├── http.go           // декодеры, энкодеры, codeFrom, kithttp.NewServer
│   │   └── grpc.go           // grpcServer, pb-кодеки, metadata-функции
│   └── platform/
│       ├── db.go             // репозиторий: интерфейс из 03 + реализация
│       ├── tracer.go         // Jaeger: сборка tracer по канону урока 08
│       └── consul.go         // клиент и Registrar из урока 12
├── pb/                       // сгенерировано protoc (урок 13) — руками не править
└── go.mod

Один взгляд на дерево отвечает на вопрос «где что живёт». Каждый каталог — один слой курса, и в имени каталога читается урок: service — 03, endpoint — 02, transport — 04 и 13, platform — 08 и 12. Три файла middleware.go — не опечатка, а осознанное разделение: сервисные декораторы (ServiceMiddleware из урока 11) живут в service, endpoint-обёртки (loggingMiddleware и instrumentingMiddleware из уроков 06-07) — в endpoint. Тесты из урока 16 кладутся рядом с пакетом: endpoint_test.go, service_test.go — штатная конвенция Go, тесты не разбрасывают по отдельному каталогу test.

internal/service: самое скучное и самое дорогое#

Пакет service содержит то, что урок 03-service называл самым ценным слоем: интерфейс StringService, бизнес-реализацию stringService, sentinel-ошибки ErrEmpty и ErrDBUnavailable, а теперь и сервисные декораторы. Помнишь запреты урока 03: никаких *http.Request, json-тегов, чтения env в глубине метода, тайных обращений к os — раскладка превращает эти запреты из дисциплины в физику. Пакет service импортирует стандартную библиотеку и platform-интерфейсы — и больше ничего из проекта; чтобы проверить чистоту, достаточно взглянуть на блок импортов. Сюда же переезжают ServiceMiddleware-декораторы: прокси-middleware из урока 11-lb-proxy с её makeUppercaseProxy и proxyingMiddleware — она описана типом ServiceMiddleware и потому по паспорту живёт в сервисе; декораторы из урока 15-service-middleware — тоже сюда. Файл service/middleware.go не импортирует endpoint и transport — он обёртывает интерфейс StringService, и только. Если в сервисный пакет просится импорт kithttp или pb — это сигнал, что логика уехала не в свой слой; верни её на место, пока раскладка не сгнила.

internal/endpoint: пары, Failer и Set#

Пакет endpoint — это всё, что построено на универсальной абстракции из урока 02-endpoint: request/response-пары UppercaseRequest и UppercaseResponse, Failer с Err-полем, конструкторы makeUppercaseEndpoint и makeCountEndpoint, которые принимают StringService и возвращают endpoint.Endpoint. Разрешённый импорт ровно один: internal/service — ради интерфейса и sentinel-ошибок; именно так зависимость течёт от транспорта к ядру, и цикл невозможен физически. Сюда же ложится endpoint/middleware.go со всеми обёртками из уроков 06-10: loggingMiddleware, instrumentingMiddleware, трейсинг-обёртка из урока 08, лимитер из урока 09, breaker из урока 10 — и то, что об них подробнее расскажет урок 14-endpoint-middleware, где обёртки собираются по правилам пакета endpoint. А чтобы main не собирал цепочки вручную для каждого метода, заведи Set — структуру Endpoints с полями Uppercase и Count и конструктором New(svc, logger, tracer, ...), который строит все endpoint'ы и навешивает обёртки в канонном порядке урока 06 → 10. Помнишь третий способ композиции из урока 02 — срез endpoint.Middlewares, который «собирается по флагам запуска или конфигу»? Конструктор Set — место, где этот срез применяется; конфигурация набора обёрток — тема следующего урока.

internal/transport: единственный потребитель pb#

Пакет transport — главный потребитель всего, что построено выше: здесь декодеры и энкодеры JSON из урока 04-http-transport, таблица codeFrom из урока 05, монтаж kithttp.NewServer на mux, а также gRPC-половина из урока 13-grpc-transport: адаптер grpcServer со встроенным Unimplemented, пары pb-кодеков, metadata-функции requestIDToContext и requestIDToMetadata. Ключевое правило раскладки: сгенерированный pb-пакет импортирует ТОЛЬКО transport — ни сервис, ни endpoint'ы не знают о protobuf, и анти-коррупционный шов из урока 13 совпадает с границей каталога. Обрати внимание: в http.go и grpc.go нет бизнес-логики и не должно появиться — транспорт только переводит диалекты, как гласил первый урок; если декодер начал принимать бизнес-решения, унеси их в сервис. Второй транспорт не обязан жить в одном пакете с первым — по мере роста можно разнести на transport/http и transport/grpc: у них нет общих типов, кроме endpoint'ов, и слияние было удобством, а не необходимостью.

internal/platform: зависимости снаружи#

Пакет platform — единственное место, которому разрешено знать о внешнем мире: репозиторий с реальным хранилищем за интерфейсом из урока 03, сборка Jaeger-трейсера по канону урока 08 (opentracing, Sampler, SetGlobalTracer), консул-клиент api.NewClient и обёртка consulsd.NewRegistrar из урока 12-discovery. Смысл выделения простой: всё, что требует сторонних модулей, сетей и credentials, концентрируется в одном тонком слое, а остальной код зависит от интерфейсов. Сервис принимает repo по интерфейсу — в тесте из урока 03 подставляется заглушка, в main — platform-реализация; трейсер передаётся как opentracing.Tracer, о котором middleware из урока 08 знают только интерфейс. В маленьком сервисе platform может показаться бюрократией — два файла; но сравни переезд с SQLite на Postgres при плоской структуре и при выделенном platform: во втором случае меняется один файл, а diff остального проекта пуст. Раскладка — это инвестиция в день, когда придётся менять внешнее, не трогая внутреннее.

main как композиционный корень

GO
func main() {
        if err := run(); err != nil {
                logger.Log("during", "run", "err", err)
                os.Exit(1)
        }
}

func run() error {
        // 1. конфигурация (урок 18): флаги и env
        cfg, err := config.Flags()
        if err != nil { return err }

        // 2. наблюдаемость: logger из 06, tracer из 08
        logger := kitlog.NewJSONLogger(os.Stderr)
        tracer, closer, err := platform.NewTracer(cfg)
        if err != nil { return err }
        defer closer.Close()

        // 3. зависимости: repo и registrar из platform
        repo := platform.NewRepo(cfg, logger)
        registrar := platform.NewConsulRegistrar(cfg, logger)

        // 4. сервис + сервисные декораторы (уроки 11, 15)
        svc := service.New(repo, logger)
        svc = service.ProxyingMiddleware(...)(svc)

        // 5. endpoint'ы + обёртки 06-10 одним конструктором
        eps := endpoint.New(svc, logger, tracer)

        // 6. транспорты над одним Set
        httpHandler := transport.NewHTTPHandler(eps, logger)
        grpcServer := transport.NewGRPCServer(eps, logger, tracer)

        // 7. слушатели, регистрация в Consul, serve
        return serve(cfg, logger, registrar, httpHandler, grpcServer)
}

Композиционный корень — единственное место, где ВСЁ знает обо ВСЁМ: здесь типы переходят из пакета в пакет, и только здесь импортируются все internal-пакеты разом. Фазы пронумерованы не для красоты: сборка идёт от периферии к ядру — config, наблюдаемость, зависимости, сервис, endpoint'ы, транспорты, запуск; в уроках 18 и 19 эти же фазы станут скелетом конфигурирования и остановки. Обрати внимание на run() error вместо плоского main: ошибки сборки возвращаются, deferred-закрытия срабатывают при любом исходе, а main остаётся трёхстрочной обёрткой — этот приём окупится в уроке 19, когда при остановке надо будет вычистить ресурсы по всем путям выхода.

Импорты текут в одну сторону#

Главный закон раскладки — направление импортов: transport импортирует endpoint, endpoint импортирует service, service не импортирует никого из проекта. Обратная стрелка — не стиль, а ошибка компиляции: если service вдруг импортирует endpoint, а endpoint импортирует service, Go откажется собирать проект с import cycle not allowed — и это лучшая сторожевая собака, которую можно придумать: канон урока 03 «сервис не знает про транспорт» охраняется компилятором, а не код-ревью. Раскладка помогает и в обратную сторону: пакеты internal не доступны внешним модулям, поэтому твой внутренний API нельзя случайно затащить в соседний проект и потом сломать его рефакторингом. Практическое правило при споре «куда положить файл»: посмотри, кто должен его импортировать; если ответ «все» — это platform или main; если «только transport» — transport; если «сервис и endpoint'ы» — service. Правило импортов дешевле любого архитектурного документа: один взгляд на блок import в файле отвечает на вопрос, на своём ли месте файл находится.

Куда положить decodeUppercaseRequest — декодер, читающий JSON из *http.Request?

Что даёт расположение пакетов под internal/?

В main.go появилась строка svc = service.ProxyingMiddleware(...)(svc). Что это по раскладке и по курсу?

Цикл импортов

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

// internal/endpoint/endpoint.go (как задумано):
import "stringsvc/internal/service"

// А теперь разработчик добавил в internal/service/middleware.go:
import "stringsvc/internal/endpoint"

func WithMetrics(next service.StringService) service.StringService {
        eps := endpoint.New(next, nil, nil) // «удобно переиспользовать»
        ...
}

env в глубине сервиса

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

// internal/service/service.go
func (stringService) Uppercase(ctx context.Context, s string) (string, error) {
        if os.Getenv("STRINGSVC_DISABLE_CACHE") == "1" {
                return strings.ToUpper(s), nil // обход кеша по переменной
        }
        ...

Собери композиционный корень

Восстанови пять строк main. Пропуски — по порядку: конструктор конфигурации, конструктор сервиса, конструктор Set endpoint'ов, kithttp-конструктор из урока 04, сгенерированная связка каркаса gRPC с реализацией.

func run() error { cfg, err := config.() if err != nil { return err } svc := service.(repo, logger) svc = service.ProxyingMiddleware(...)(svc) eps := endpoint.(svc, logger, tracer) h := kithttp.(eps.Uppercase, decodeUppercaseRequest, encodeUppercaseResponse) grpcServer := transport.NewGRPCServer(eps, logger, tracer) _ = h return serve(cfg, logger, func(s *grpc.Server) { pb.(s, grpcServer) }) }

Сборка сервиса в композиционном корне

Расставь фазы run() в правильном порядке: от периферии к ядру и обратно к запуску.

Строки

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

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

Карта раскладки go-kit проекта#

ПутьЧто живётУрокиЗдесь запрещено
cmd/stringsvc/main.goкомпозиционный корень: config, сборка, слушатели01, 17бизнес-логика
internal/service/service.goинтерфейс StringService, реализация, sentinel-ошибки03net/http, pb, env в методах
internal/service/middleware.goServiceMiddleware: прокси из 11, декораторы из 1511, 15импорт endpoint и transport
internal/endpoint/endpoint.gorequest/response-пары, Failer, make*Endpoint, Set02json-теги на доменных типах
internal/endpoint/middleware.goобёртки: логи, метрики, спаны, лимитер, breaker06-10, 14бизнес-решения
internal/transport/http.goдекодеры, энкодеры, codeFrom, mux, kithttp.NewServer04, 05интерфейс сервиса
internal/transport/grpc.gogrpcServer, pb-кодеки, metadata-функции13интерфейс сервиса
internal/platformрепозиторий, Jaeger-tracer, consul-регистратор08, 12доменные правила
pb/сгенерировано protoc: pb-структуры, ServiceDesc13ручные правки

Правила выживания большого проекта#

  1. Импорты текут в одну сторону: transport → endpoint → service; обратная стрелка — ошибка компиляции, и это благо.
  2. main не содержит логики, сервис не собирает зависимости: сборка — только в композиционном корне.
  3. pb/ не редактируется руками: правится .proto, потом protoc; иначе контракт из урока 13 разойдётся с кодом.
  4. Тесты лежат рядом с пакетом: service_test.go тестирует сервис без транспорта, endpoint_test.go — endpoint'ы, как в уроке 16.
  5. Новая фича добавляется снизу вверх: интерфейс сервиса, реализация, endpoint, декодер — тогда раскладка не размывается.
  6. Папки utils и helpers не создавать: если пакет не может назвать свою ответственность одним словом, он не нужен.

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

Термин

Композиционный корень

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

cmd/stringsvc/main.go: единственное место, где собираются config, logger, tracer, repo, сервис, endpoint'ы и транспорты; логики не содержит, только сборку.

1 / 6

Что дальше#

Раскладка собрана: service из урока 03, endpoint'ы из урока 02, два транспорта из уроков 04 и 13 и platform из уроков 08 и 12 разошлись по пакетам, импорты текут в одну сторону, main стал коротким композиционным корнем. Но всмотрись в него внимательнее: строки ":8081", ":8082", "localhost:8500" и процент сэмплинга Jaeger всё ещё зашиты в код — собранный бинарь работает только на твоей машине. Урок 18-konfiguraciya вынесет эти значения наружу: флаги запуска, env-переменные, безопасные дефолты и fail fast на невалидной конфигурации — так, чтобы один артефакт жил во всех средах, от ноутбука до продакшена. Освежить слои можно на странице курса, а деталь про срез endpoint.Middlewares, который «собирается по флагам» — в уроке 02-endpoint: следующий урок даст этим словам буквальный смысл.

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

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

Вывод

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

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

  • Структура go kit проекта — производная слоёв: service, endpoint, transport, platform превращаются в пакеты internal, точка сборки живёт в cmd.
  • go-kit не навязывает раскладку — библиотека без каркаса; раскладку диктуют сами слои и направление импортов transport → endpoint → service.
  • internal/ даёт встроенную защиту Go: пакеты недоступны внешним модулям, внутренний API не расползается по чужим проектам.
  • Композиционный корень — единственное место, где всё знает обо всём; фазы сборки: config → наблюдаемость → зависимости → сервис → endpoint'ы → транспорты → запуск.
  • pb-пакет импортирует только transport: анти-коррупционный шов из урока 13 совпадает с границей каталога.
  • Сервисные декораторы (ServiceMiddleware из уроков 11 и 15) живут в service/middleware.go, endpoint-обёртки из уроков 06-10 — в endpoint/middleware.go.
  • Import cycle not allowed — ошибка компиляции, охраняющая канон урока 03: сервис не импортирует endpoint и transport.
  • Папки models, utils, helpers не создаются: пакет без одной ясной ответственности — будущая помойка.

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

Разложи stringsvc по канону урока. Первое: создай cmd/stringsvc, internal/service, internal/endpoint, internal/transport, internal/platform и pb и перенеси код из God-файла по дереву — прокси-middleware из урока 11 в service/middleware.go, обёртки из уроков 06-10 в endpoint/middleware.go, декодеры и gRPC-адаптер в transport. Второе: собери Set — структуру Endpoints с конструктором New(svc, logger, tracer), который строит оба endpoint'а и навешивает обёртки в порядке урока 06 → 10; main должен собрать сервис двумя строками. Третье: докажи компилятором канон урока 03 — добавь в service/middleware.go импорт internal/endpoint, запусти go build ./..., сохрани текст ошибки import cycle в конспект и убери импорт. Четвёртое: проверь, что тесты из урока 03 и 16 продолжают собираться рядом с пакетами без транспорта, и запусти go test ./... — зелёно значит, что логика не уехала из слоёв. Пятое: посмотри на оставшийся main и выпиши в конспект все зашитые значения — порты, адрес Consul, долю сэмплинга; список пригодится в уроке 18 как входной инвентарь для конфигурирования.

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

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

Нужен ли отдельный пакет для middleware, если файлов становится много?

Три файла middleware.go в уроке — не предел, но и не повод для нового пакета: сервисные декораторы остаются в service, endpoint-обёртки в endpoint. Новый пакет оправдан, когда появляется собственная доменная абстракция с типами, например отдельный лимитер с конфигурацией. Разводить пакеты по техническим словам вроде middleware верхнего уровня не стоит — направление импортов важнее количества файлов.

Куда класть сгенерированный pb-пакет — в internal или рядом?

В нашей раскладке pb/ лежит на верхнем уровне и импортируется только internal/transport. На верхнем уровне он потому, что генерация командой protoc с go_package не должна селиться внутри internal по соображениям удобства путей, а не видимости: импортировать pb напрямую всё равно не станет ни один слой по правилам курса. Если хочешь жёстче — положи pb в internal/pb, компилятор не возражает.

Как раскладка связана с уроками 14-16 про middleware и тестирование?

Прямо: обёртки, которые подробно разбирает урок 14-endpoint-middleware, кладутся в endpoint/middleware.go, декораторы сервиса из урока 15-service-middleware — в service/middleware.go, а тесты из урока 16-testirovanie — рядом с пакетами, которые проверяют. Раскладка не добавляет новых механик: она даёт урокам 02-13 адреса и не даёт слоям перепутаться при росте команды.

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

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

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

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

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

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

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