Содержание урока · 13 разделов
- От God-файла к раскладке
- Каноническая раскладка: cmd, internal, pb
- internal/service: самое скучное и самое дорогое
- internal/endpoint: пары, Failer и Set
- internal/transport: единственный потребитель pb
- internal/platform: зависимости снаружи
- Импорты текут в одну сторону
- Карта раскладки go-kit проекта
- Правила выживания большого проекта
- Что дальше
- Песочница урока
- Коротко: что запомнить
- Вопросы и ответы
От 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
GOstringsvc/
├── 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 как композиционный корень
GOfunc 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 с реализацией.
Сборка сервиса в композиционном корне
Расставь фазы run() в правильном порядке: от периферии к ядру и обратно к запуску.
Строки
Ваша программа
Нажимайте на строки слева, чтобы собрать программу
Карта раскладки go-kit проекта#
| Путь | Что живёт | Уроки | Здесь запрещено |
|---|---|---|---|
| cmd/stringsvc/main.go | композиционный корень: config, сборка, слушатели | 01, 17 | бизнес-логика |
| internal/service/service.go | интерфейс StringService, реализация, sentinel-ошибки | 03 | net/http, pb, env в методах |
| internal/service/middleware.go | ServiceMiddleware: прокси из 11, декораторы из 15 | 11, 15 | импорт endpoint и transport |
| internal/endpoint/endpoint.go | request/response-пары, Failer, make*Endpoint, Set | 02 | json-теги на доменных типах |
| internal/endpoint/middleware.go | обёртки: логи, метрики, спаны, лимитер, breaker | 06-10, 14 | бизнес-решения |
| internal/transport/http.go | декодеры, энкодеры, codeFrom, mux, kithttp.NewServer | 04, 05 | интерфейс сервиса |
| internal/transport/grpc.go | grpcServer, pb-кодеки, metadata-функции | 13 | интерфейс сервиса |
| internal/platform | репозиторий, Jaeger-tracer, consul-регистратор | 08, 12 | доменные правила |
| pb/ | сгенерировано protoc: pb-структуры, ServiceDesc | 13 | ручные правки |
Правила выживания большого проекта#
- Импорты текут в одну сторону: transport → endpoint → service; обратная стрелка — ошибка компиляции, и это благо.
- main не содержит логики, сервис не собирает зависимости: сборка — только в композиционном корне.
- pb/ не редактируется руками: правится .proto, потом protoc; иначе контракт из урока 13 разойдётся с кодом.
- Тесты лежат рядом с пакетом: service_test.go тестирует сервис без транспорта, endpoint_test.go — endpoint'ы, как в уроке 16.
- Новая фича добавляется снизу вверх: интерфейс сервиса, реализация, endpoint, декодер — тогда раскладка не размывается.
- Папки utils и helpers не создавать: если пакет не может назвать свою ответственность одним словом, он не нужен.
Конвенция Go знает ещё каталог pkg/ — «можно импортировать извне», — и в контексте go-kit сервиса он почти всегда лишний: у внутреннего сервиса нет внешних потребителей, а значит, нет и права на публичный API. Ошибка новичков — свалить переиспользуемое в pkg/, чтобы «было доступно другим сервисам»; другими сервисами оно становится через собственную библиотеку с версионированием, а не через импорт чужого internal. Пока не появился реальный второй потребитель — держи всё в internal и усложняй только по факту.
Урок — про структуру, и его материал проверяется компиляцией и чтением: в песочнице курса сторонние модули и процессы не исполняются, поэтому интерактивы построены на чтении кода. Локально разложи stringsvc по дереву из урока, собери go build ./... и попробуй нарочно устроить import cycle — увидишь ошибку компилятора и запомнишь границу слоёв лучше, чем из текста.
Термины урока
Что дальше#
Раскладка собрана: 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 как входной инвентарь для конфигурирования.
Личные заметкиПусто
Вопросы и ответы по уроку
Нужен ли отдельный пакет для 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 адреса и не даёт слоям перепутаться при росте команды.
Термины урока
Ключевые понятия из этого урока — короткие определения в глоссарии. Наведите на термин, чтобы вспомнить, откройте карточку для подробностей и ссылки на практику.
Изучайте дальше
Следующая ступень учебного пути
gRPC-Go: высокопроизводительный RPC
Курс gRPC-Go: proto3, кодогенерация, unary и streaming вызовы, interceptors, TLS, дедлайны, load balancing. 25 уроков и итоговый проект.
- Писать .proto-схемы и генерировать код
- Реализовывать unary и streaming сервисы
- Управлять метаданными, дедлайнами и отменами
- Строить interceptors для логов и аутентификации