---
title: "Структура go-kit проекта"
description: "Раскладка go-kit проекта: internal/service, endpoint и transport, композиционный корень в cmd, направление импортов и шаблон большого сервиса."
course: "go-kit: микросервисный тулкит"
url: "https://golangmaster.ru/courses/go-kit/lessons/17-struktura/"
published: "2026-09-19"
updated: "2026-09-21"
minutes: 20
---

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

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

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

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

Открой main.go, который мы писали с [урока 01-filosofiya](/courses/go-kit/lessons/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](/courses/go-kit/lessons/03-service/) называл самым ценным слоем: интерфейс StringService, бизнес-реализацию stringService, sentinel-ошибки ErrEmpty и ErrDBUnavailable, а теперь и сервисные декораторы. Помнишь запреты урока 03: никаких *http.Request, json-тегов, чтения env в глубине метода, тайных обращений к os — раскладка превращает эти запреты из дисциплины в физику. Пакет service импортирует стандартную библиотеку и platform-интерфейсы — и больше ничего из проекта; чтобы проверить чистоту, достаточно взглянуть на блок импортов. Сюда же переезжают ServiceMiddleware-декораторы: прокси-middleware из [урока 11-lb-proxy](/courses/go-kit/lessons/11-lb-proxy/) с её makeUppercaseProxy и proxyingMiddleware — она описана типом ServiceMiddleware и потому по паспорту живёт в сервисе; декораторы из урока 15-service-middleware — тоже сюда. Файл service/middleware.go не импортирует endpoint и transport — он обёртывает интерфейс StringService, и только. Если в сервисный пакет просится импорт kithttp или pb — это сигнал, что логика уехала не в свой слой; верни её на место, пока раскладка не сгнила.

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

Пакет endpoint — это всё, что построено на универсальной абстракции из [урока 02-endpoint](/courses/go-kit/lessons/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](/courses/go-kit/lessons/04-http-transport/), таблица codeFrom из урока 05, монтаж kithttp.NewServer на mux, а также gRPC-половина из [урока 13-grpc-transport](/courses/go-kit/lessons/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](/courses/go-kit/lessons/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/service — он же часть обработки запроса
- internal/endpoint — рядом с endpoint'ом
- ✅ internal/transport — транспортные типы легальны только здесь
- cmd/stringsvc — раз вызывается из main

Декодер принимает *http.Request и json-теги — это транспортные типы, которым по канону урока 05 и правилам раскладки место только в transport. Сервис про HTTP не знает никогда, endpoint-пакет работает с чистыми request/response-парами и не должен тянуть net/http, а main вообще ничего не обрабатывает — он только собирает. Проверка простая: если файл импортирует net/http или pb — он живёт в transport.

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

- Ускоряет компиляцию за счёт скрытых пакетов
- ✅ Компилятор запрещает импорт этих пакетов из внешних модулей
- Go-инструменты автоматически тестируют такие пакеты
- Внутри internal нельзя объявлять интерфейсы

internal — встроенная в Go механика видимости: пакеты внутри internal/… доступны только коду того же модуля. Для библиотеки это защита пользователей от не-API-поверхности, для сервиса — гарантия, что внутренние типы не расползутся по чужим проектам. На скорость компиляции, тесты и интерфейсы каталог не влияет никак — только на границы импорта.

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

- Endpoint-обёртка из урока 06 — ошибка, ей место в endpoint/middleware.go
- ✅ Сервисный декоратор ServiceMiddleware из урока 11 — корректно: он обёртывает интерфейс сервиса
- Транспортная опция — надо переносить в transport
- Функция main — нельзя обёртывать сервис вне композиционного корня

Прокси из урока 11 описан типом ServiceMiddleware: он принимает и возвращает интерфейс StringService, не касаясь endpoint'ов и транспортов. По раскладке ему место в service/middleware.go, а вызов — в композиционном корне до сборки endpoint'ов: сначала оборачивается сервис, потом из обёрнутого сервиса строятся endpoint'ы и навешиваются endpoint-обёртки 06-10. Порядок «сервисные декораторы, затем endpoint'ы» — канон сборки курса.

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

```go
// 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) // «удобно переиспользовать»
        ...
}
```

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

- Проект соберётся: Go разрешает взаимные импорты пакетов
- Проект соберётся, но появится скрытая зависимость на транспорт
- ✅ Компиляция упадёт: import cycle not allowed
- Соберётся только тесты, бинарь не соберётся

Go запрещает циклы импортов на уровне компилятора — и в этой строгости вся польза раскладки: попытка сервиса переиспользовать endpoint'ы мгновенно красит сборку, а не ждёт код-ревью. Лечится не разрывом запрета, а возвращением логики на слой: метрики сервиса — это ServiceMiddleware в service/middleware.go, как в уроках 11 и 15, без захода в endpoint-пакет. Если общей кодовой базе нужна одна и та же функция на двух слоях — выноси её в отдельный маленький пакет ниже обоих, а не создавай цикл.

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

```go
// 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 // обход кеша по переменной
        }
        ...
```

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

- Ничего не нарушено: сервис просто прочитал переменную
- Сломается компиляция: service не может импортировать os
- ✅ Канон урока 03 нарушен: зависимость спрятана в методе — тест сервиса перестаёт собираться одной строкой и ведёт себя по-разному в разных средах
- Сломается только при graceful shutdown

Урок 03 запрещал чтение env в глубине метода не из педантизма: это скрытая зависимость, о которой знает только среда исполнения. Тест, который собирался строкой svc := stringService{}, теперь зависит от окружения CI; поведение сервиса невозможно понять из конструктора. Правильный путь — конфигурация: значение читается в main при сборке (тема урока 18) и передаётся в сервис параметром конструктора или декоратором. Раскладка и канон 03 говорят одно и то же: зависимости — через композиционный корень.

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

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

```go
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)
        })
}
```

Подсказка: Первый и второй пропуски — конструкторы из пакетов config и service; третий собирает ВСЕ endpoint'ы с обёртками разом; четвёртый — kithttp.NewServer из урока 04; пятый — RegisterStringsServer из урока 13.

Правильные ответы в пропусках: `Flags`, `New`, `New`, `NewServer`, `RegisterStringsServer`

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

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

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

1. `Разобрать конфигурацию: флаги и env собраны в Config`
2. `Построить наблюдаемость: logger из урока 06 и tracer из урока 08`
3. `Открыть зависимости: репозиторий и consul-регистратор из platform`
4. `Собрать сервис: service.New(repo, logger)`
5. `Обернуть сервис декораторами ServiceMiddleware из уроков 11 и 15`
6. `Собрать Set endpoint'ов: конструктор навесил обёртки из уроков 06-10`
7. `Построить транспорты: kithttp.NewServer и grpcServer над одним Set`
8. `Поднять слушатели :8081 и :8082 и вызвать registrar.Register()`

### Карта раскладки 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 | ручные правки |

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

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 не создавать: если пакет не может назвать свою ответственность одним словом, он не нужен.

> **pkg/ — только осознанно.** Конвенция Go знает ещё каталог pkg/ — «можно импортировать извне», — и в контексте go-kit сервиса он почти всегда лишний: у внутреннего сервиса нет внешних потребителей, а значит, нет и права на публичный API. Ошибка новичков — свалить переиспользуемое в pkg/, чтобы «было доступно другим сервисам»; другими сервисами оно становится через собственную библиотеку с версионированием, а не через импорт чужого internal. Пока не появился реальный второй потребитель — держи всё в internal и усложняй только по факту.

> **Честно о запуске кода.** Урок — про структуру, и его материал проверяется компиляцией и чтением: в песочнице курса сторонние модули и процессы не исполняются, поэтому интерактивы построены на чтении кода. Локально разложи stringsvc по дереву из урока, собери go build ./... и попробуй нарочно устроить import cycle — увидишь ошибку компилятора и запомнишь границу слоёв лучше, чем из текста.

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

- **Композиционный корень** — cmd/stringsvc/main.go: единственное место, где собираются config, logger, tracer, repo, сервис, endpoint'ы и транспорты; логики не содержит, только сборку.
- **internal/** — Каталог с встроенной защитой Go: пакеты доступны только коду того же модуля; чужие проекты импортировать их не могут.
- **Import cycle** — Ошибка компиляции import cycle not allowed при взаимных импортах; в раскладке курса сторожит канон «сервис не знает про транспорт».
- **Set endpoint'ов** — Структура Endpoints{Uppercase, Count} с конструктором New(svc, logger, tracer): строит все endpoint'ы и навешивает обёртки 06-10 в канонном порядке одним вызовом.
- **ServiceMiddleware** — Тип func(StringService) StringService: декораторы сервиса — прокси из урока 11 и мидлвари урока 15; живут в service/middleware.go.
- **platform** — Пакет внешних зависимостей: репозиторий, Jaeger-tracer, consul-регистратор; остальной код зависит от интерфейсов, а не от клиентов.

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

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

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

- Структура 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 адреса и не даёт слоям перепутаться при росте команды.
