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

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

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

Урок 23 из 25 ~18 мин +50 XP

Лучшая структура проекта

структура проекта connect — тема, которую второй урок честно отложил: тогда мы собирали минимальный скелет proto, gen и cmd, а обещание разобрать взрослую раскладку оставили на финал курса. Обещание исполняю: если собрать частые запросы — connect структура, grpc проект структура, buf проект структура, — получится ровно карта этого урока. Раскладываем по этажам контракт proto/, генерат gen/, service-слой с хендлерами и хранилищем, сквозную платформу и композиционный корень cmd; фиксируем закон импортов, который не даёт проекту сгнить; проходим путь фичи от правки схемы до PR и смотрим, как раскладка переживает второй сервис и вторую версию. К концу урока у тебя будет шаблон, который открывается в редакторе и не требует пояснений в чате команды.

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

Во втором уроке каноническая минимальная структура держалась на трёх ролях: схемы редактируешь руками, генерат не трогаешь никогда, свой код пишешь всегда. Тот скелет — proto/, gen/ и cmd/server — честно работает, пока проект учебный. Но рабочий сервис из девятнадцатого урока уже тянет за собой healthz, readyz и metrics на общем mux, интерсепторы из седьмого и тринадцатого уроков, хранилище, конфигурацию из окружения, а деплой из двадцать второго добавляет Dockerfile и CI. Всё это нельзя сваливать в один каталог: main.go разрастается до тысячи строк, и правка интерсептора начинается с получаса поиска нужной функции. Взрослый шаблон добавляет к трём ролям два этажа — service-слой со своей логикой и платформу со сквозным, — и главное, фиксирует направление зависимостей между ними. План урока: дерево проекта целиком, каждый этаж по отдельности, закон импортов, таблица раскладки, путь фичи от схемы до PR и то, как шаблон растёт вместе с командой. Второй урок обещал эту тему ближе к концу курса — обещание исполняется здесь и сейчас.

Дерево рабочего шаблона

GO
notes-api/
├── buf.yaml              # паспорт модуля: path proto, lint, breaking
├── buf.gen.yaml          # рецепт генерации: плагины, out: gen
├── go.mod                # module example.com/notes-api
├── proto/                # КОНТРАКТ — редактируешь только здесь
│   └── notes/v1/notes.proto
├── gen/                  # ГЕНЕРАТ — пишет только buf generate
│   └── notes/v1/
│       ├── notes.pb.go
│       └── notesv1connect/notes.connect.go
├── internal/
│   ├── notes/            # SERVICE-слой: хендлеры, домен, хранилище
│   │   ├── server.go     # notesServer: реализация handler-интерфейса
│   │   ├── store.go      # интерфейс хранилища + sentinel-ошибки
│   │   └── memory.go     # in-memory реализация для тестов и локалки
│   └── platform/         # СКВОЗНОЕ: auth, метрики, логи, рефлексия
│       ├── auth.go
│       └── metrics.go
└── cmd/notes/main.go     # КОМПОЗИЦИОННЫЙ КОРЕНЬ: сборка всего

Пять этажей и два конфига в корне. Комментарии — это контракты каталогов: любой новый файл обязан знать своё место до того, как написан. internal/ — встроенная защита Go: пакеты из него не импортирует чужой модуль, поэтому домен не «утекает» в соседние репозитории.

proto/: контракт как первый гражданин#

Каталог proto/ — единственное место, где ты редактируешь контракты, и buf.yaml в корне делает этот каталог модулем: секция modules с path: proto отсчитывает все пути от него, поэтому файл proto/notes/v1/notes.proto для buf — это notes/v1/notes.proto. Каталог внутри proto/ повторяет имя пакета: notes.v1 живёт в notes/v1/, и версия — часть пути, а не комментарий. buf.yaml же держит политику качества: lint с набором STANDARD ловит стиль схем до генерации, секция breaking задаёт правила обратной совместимости для CI. Комментарии над service и rpc — не украшение: из пятнадцатого урока ты знаешь, что из них собираются доки, OpenAPI и ответы рефлексии, поэтому контракт редактируется вместе с документацией, в одном коммите. Правило гигиены: в proto/ нет ничего, кроме схем, — ни моков, ни утилит, ни Go-файлов. Если в схеме появилась языковая опция, подумай дважды: managed mode из одиннадцатого урока умеет проставлять go_package_prefix за все языки сразу, оставляя схемы чистыми. Публикация контракта наружу — buf push с меткой из двенадцатого урока, когда потребители хотят версию без клона твоего репозитория.

buf.yaml и buf.gen.yaml в корне

GO
# buf.yaml — паспорт модуля
version: v2
modules:
  - path: proto
lint:
  use: [STANDARD]
breaking:
  use: [FILE]

# buf.gen.yaml — рецепт генерации
version: v2
plugins:
  - local: protoc-gen-go
    out: gen
    opt: paths=source_relative
  - local: protoc-gen-connect-go
    out: gen
    opt: paths=source_relative

Оба конфига живут в корне рядом с go.mod: buf generate без аргументов читает их сам. out: gen и paths=source_relative дают путь gen/notes/v1/notes.pb.go — ровно тот, что зашит в go_package схемы. Взрослый вариант заменяет local на remote-плагины с закреплённой версией — тогда CI не ставит тулчейн вообще.

gen/: генерат и правила сосуществования#

Каталог gen/ — территория buf generate: сюда падают notes.pb.go с message-типами и подпакет notesv1connect с handler-интерфейсом, заглушкой UnimplementedNoteServiceHandler, конструктором NewNoteServiceHandler и типизированным клиентом. Первое правило ты знаешь со второго урока: править файлы в gen/ нельзя категорически — любая перегенерация затрёт правки, а источник правды всегда один, и это схема. Второе правило — коммитить или нет — вечный спор двух лагерей, и для учебного проекта второй урок уже дал вердикт: коммить, меньше движущихся частей. Взрослая политика выглядит иначе: gen/ в .gitignore, buf generate в CI с remote-плагинами закреплённой версии, и тогда репозиторий читается без шума в диффах, а воспроизводимость гарантирует конфиг, а не память коллеги. В честном buf проект структура gen/ — артефакт сборки, а не исходник; какую из двух политик ни выбрал бы, выбери одну на всю команду и запиши её в contributing. Третье правило — границы: gen/ импортирует только runtime connectrpc.com/connect и protobuf-пакеты; ничего из internal/ сгенерированный код не видит и видеть не должен, иначе шов между контрактом и кодом перестаёт быть швом.

Коллега поправил сообщение в gen/notes/v1/notes.pb.go, чтобы «не ждать генерацию». Что произойдёт?

internal/notes: service-слой#

Здесь живёт твой код, который делает сервис сервисом. Реализация notesServer — тот тип с приёмником-указателем из третьего урока, что реализует handler-интерфейс: по методу на каждый rpc, сигнатуры со сгенерированными message-типами, состояние в полях структуры. Рядом — интерфейс хранилища Store с методами Create, Get, List, Update, Delete и sentinel-ошибки ErrNotFound и ErrForbidden: хендлер переводит их в коды через connect.NewError из шестого урока, и ни одна ошибка хранилища не утекает наружу сырым текстом. Хранилище может быть in-memory из этого же каталога, SQL-драйвером или прокси на соседний сервис — notesServer не знает и не должен знать. Домен держи в чистых Go-типах: структура Note с полями, валидации, правила владения; конвертация домен-контракт занимает одну строку на поле, и эта плата за чистоту окупается, когда появится вторая версия схемы из восемнадцатого урока и общий домен станет общим честно. Тесты слоя лежат рядом: service_test.go с table-driven матрицей кодов — канон шестнадцатого урока.

internal/platform: сквозное, не знающее домена#

Платформа собирает то, что должно работать на каждом вызове, не зная ни одного message-типа. Auth-интерсептор из тринадцатого урока: читает Authorization, проверяет токен, кладёт владельца в контекст, отказывает unauthenticated — его интересуют заголовки и процедуры, а не заметки. Метрики из семнадцатого: счётчик по меткам procedure и code, гистограмма длительности — интерсептор работает с req.Spec().Procedure и connect.CodeOf(err), домен ему не нужен. Логирующий интерсептор с request id, otelconnect для трейсов, монтаж gRPC-рефлексии из пятнадцатого урока — всё это пакеты platform, которые можно взять и переиспользовать в следующем сервисе проекта. CORS для браузерных клиентов из девятого урока — тоже platform: middleware на mux, белый список origin из конфига. Ключевое отличие платформы от service-слоя: она обязана собираться без импорта notes-пакета. Если платформенному файлу понадобился notesv1 — ты пишешь не платформу, а бизнес-логику не в том месте, и это ранний сигнал переставить код. Именно так интерфейсные типы AnyRequest и AnyResponse из седьмого урока окупаются архитектурой: один интерсептор на все сервисы и версии.

cmd/notes/main.go: композиционный корень

GO
func main() {
    store := notesmemory.New()
    srv := &notesserver.Server{Store: store}

    mux := http.NewServeMux()
    mux.HandleFunc("GET /healthz", health)
    mux.HandleFunc("GET /readyz", ready(store))
    mux.Handle("GET /metrics", promhttp.Handler())

    path, handler := notesv1connect.NewNoteServiceHandler(
        srv,
        connect.WithReadMaxBytes(1<<20),
        connect.WithCompressMinBytes(1024),
        connect.WithInterceptors(&metrics.Interceptor{}, &auth.Interceptor{}),
    )
    mux.Handle(path, handler)

    httpSrv := &http.Server{
        Addr:    ":8080",
        Handler: h2c.NewHandler(mux, &http2.Server{}),
    }
    // запуск, сигнал SIGTERM и srv.Shutdown — как в девятнадцатом уроке
    _ = httpSrv
}

main — единственное место, где видны все пакеты сразу: хранилище, сервер, платформа, генерат. Опции лимитов и сжатия из четырнадцатого урока, h2c из девятнадцатого, пробные роуты из того же урока — композиционный корень собирает канон курса в один файл, и других мест с импортом всего и вся в проекте нет.

Куда положить интерсептор, который проверяет Bearer-токен и кладёт владельца в контекст?

Закон импортов: направление, которое нельзя развернуть#

Структура без правил зависимостей — это просто папки. Закон этого шаблона читается по стрелкам: cmd импортирует всех, internal/notes импортирует gen и стандартную библиотеку, internal/platform импортирует только connect и общие пакеты, gen не импортирует ничего из internal. Обратное направление запрещено: платформа не знает домена, домен не знает платформы, и если оба правила держать, import cycle невозможен физически — компилятор будет ругаться раньше, чем ревьюер. gen — граница: только cmd и internal/notes имеют право импортировать notesv1 и notesv1connect; появились такие импорты в platform — код лежит не в своём этаже. Проверка закона автоматизируется: goimports держит порядок импортов, а правила типа «никаких notesv1 вне notes и cmd» ловит lint-скрипт в CI за десять строк. Отдельный запрет — каталоги utils и helpers: имя, под которым нельзя угадать содержимое, — имя, в которое складывают всё. Закон импортов — тот же принцип, что кнопки лимитов из четырнадцатого урока: каждый барьер существует, чтобы изменения текли в одну сторону, и его нарушение видно сразу.

Карта раскладки#

КаталогЧто живётПравило
proto/Схемы notes/v1, комментарии контрактаЕдинственное место ручных правок контракта; buf lint и breaking смотрят сюда
gen/notes.pb.go и notesv1connectТолько buf generate; правки затираются, политика коммита одна на команду
internal/notesnotesServer, Store, sentinel-ошибки, доменИмпортирует gen; тесты table-driven рядом
internal/platformAuth, метрики, логи, CORS, рефлексияНе знает домена: работает с Any-типами и процедурами
cmd/notesmain.go, композиционный кореньСобирает всех; единственный файл, знающий все пакеты
buf.yamlМодуль path proto, lint STANDARD, breakingМеняется при смене политики, не при каждой фиче
buf.gen.yamlПлагины и out: genRemote-плагины с закреплённой версией — воспроизводимость в CI
go.modМодуль и зависимостиconnectrpc.com/connect без grpc-go; internal закрыт от соседей
Makefile и CIЦели lint, generate, breaking, test, buildГенерация и breaking в пайплайне, не на памяти коллег

Собери монтаж сервиса в main

Впиши пропущенные вызовы: конструктор handler-а, опция интерсепторов и два способа монтирования на mux.

path, handler := notesv1connect.( &notesServer{store: store}, connect.(&metrics.Interceptor{}, &auth.Interceptor{}), ) mux.(path, handler) mux.("GET /healthz", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) })

Путь фичи от схемы до PR

Расставь шаги новой фичи в рабочем шаблоне.

Строки

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

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

Рост: второй сервис и вторая версия#

Хороший шаблон узнаётся по тому, как он переживает рост. Второй сервис: добавляешь proto/tasks/v1/tasks.proto, buf generate сам раскладывает генерат по путям, в internal появляется tasks/, а в main — вторая пара строк конструктор плюс Handle на том же mux и том же порту: третий урок показал это на eliza и notes рядом, и ничего не изменилось. Схемы разных сервисов могут жить в одном модуле, а могут разъехаться по репозиториям — тогда контракт доставляется зависимостью из BSR, и двенадцатый урок уже дал тебе механику. Вторая версия: notes.v2 живёт рядом с v1 на том же mux, маршрутизация по путям процедур разделяет их автоматически, конвертация v1-v2 занимает место в общем домене — восемнадцатый урок расписал жизненный цикл. Несколько модулей Go в одном репозитории требуют go.work — не включай его без нужды: пока пакетов меньше десятка, один go.mod проще. И главный индикатор здоровья: добавление фичи не должно требовать правок в platform. Если платформа начала меняться под каждую фичу — сквозное и доменное перепутались, и стоит перечитать закон импортов.

В команде появился второй сервис tasks.v1. Что происходит с mux и портом?

Карточки: структура проекта

Термин

Пять этажей шаблона

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

proto/ — контракт, gen/ — генерат, internal/notes — service-слой, internal/platform — сквозное, cmd/notes — композиционный корень. Плюс buf.yaml и buf.gen.yaml в корне и go.mod с модулем.

1 / 6

Что дальше по курсу#

Раскладка готова, и шаблон уже ждёт первую поломку: в двадцать четвёртом уроке — типичные ошибки и отладка curl-режимом, grpcurl и buf curl, карта симптомов от 404 до resource_exhausted и трассировка проблемного вызова по меткам и спанам. Финал курса — двадцать пятый урок: итоговый проект, который собирает всё в один Notes API. Для повторения: скелет и первые конфиги — во втором уроке, два сервиса на одном mux — в третьем, lint и breaking — в десятом, генерация и remote-плагины — в одиннадцатом, модули BSR — в двенадцатом, рефлексия и дескрипторы — в пятнадцатом, версии схем — в восемнадцатом, общий mux с пробами и h2c — в девятнадцатом. Программа всех двадцати пяти уроков — на странице курса.

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

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

Вывод

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

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

  • Рабочая структура: proto/ контракт, gen/ генерат, internal/notes — service-слой, internal/platform — сквозное, cmd/notes — композиционный корень.
  • buf.yaml делает proto/ модулем: путь и версия пакета — часть каталога; lint STANDARD и breaking FILE — политика CI.
  • gen/ — артефакт: правки затираются; коммить или генерируй в CI — одно правило на команду, remote-плагины дают воспроизводимость.
  • Service-слой: notesServer реализует handler-интерфейс, хранилище за интерфейсом Store, sentinel-ошибки маппятся в коды на границе.
  • Platform не знает домена: auth, метрики, логи, CORS и рефлексия работают с Any-типами и req.Spec().Procedure.
  • Закон импортов: gen импортируют только cmd и notes; platform без notesv1; цикл невозможен, пока стрелки не развернуты.
  • Рост без боли: второй сервис — конструктор и Handle на том же mux; v2 рядом с v1; контракт наружу — BSR.
  • Makefile с целями lint, generate, test, run — одна правда о сборке локально и в CI; тулчейн не попадает в runtime-образ.

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

Переложи notes-сервис из третьего урока в рабочий шаблон. Шаг первый: создай каталоги internal/notes и internal/platform, вынеси notesServer в internal/notes/server.go, добавь интерфейс Store с sentinel-ошибками и in-memory реализацией. Шаг второй: перенеси auth-интерсептор из тринадцатого урока в internal/platform/auth.go и добейся, чтобы пакет platform собирался без импорта notesv1. Шаг третий: собери cmd/notes/main.go с healthz, readyz, metrics, опциями лимитов из четырнадцатого урока и h2c — сервис должен запускаться одной командой go run ./cmd/notes. Шаг четвёртый: добавь Makefile с целями lint, generate, test, run и прогони их по очереди. Шаг пятый, исследовательский: попробуй импортировать notesv1 из platform и прочитай ошибку компилятора; затем попробуй вызвать platform-код из notes — убедись, что закон импортов держит тебя сильнее, чем код-ревью. Зафиксируй выбранную политику по gen/ в README: коммит или генерация в CI.

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

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

Коммитить gen/ или генерировать в CI?

Обе политики рабочие, выбирается одна на команду. Учебный проект — коммить: клон и сборка без тулчейна. Взрослая — gen в .gitignore, buf generate в CI, remote-плагины с закреплённой версией: чистые диффы и воспроизводимость. Опасно только гибрид, когда часть файлов коммитится, а часть генерируется.

Один репозиторий на сервис или монорепозиторий?

Начинай с одного репозитория и одного go.mod: пока сервисов меньше пяти, цена монорепо — издержки, а польза — атомарные PR с контрактом и кодом. Когда сервисы разъезжаются, контракты выносятся в BSR, и потребители берут схему зависимостью из двенадцатого урока. go.work нужен, только если модулей Go стало несколько.

Где должны лежать тесты?

Юнит и table-driven матрицы кодов — рядом с пакетом: internal/notes/server_test.go собирается без сети и прогоняется с -race. Полностековые тесты на httptest.NewServer — там же или в cmd. Интеграционные с реальными зависимостями — в отдельном каталоге с меткой сборки, чтобы go test ./... оставался быстрым.

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

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

Сравните с альтернативами

Практический вопрос после первых уроков: а почему именно эта библиотека? Мы разобрали конкурентов по одним критериям, с кодом одной задачи рядом и персональным подбором под ваши приоритеты.

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

Fyne25 уроков~13 чпрактика25 уроков уже доступны

Fyne: десктопные и мобильные GUI на Go

Курс Fyne: окна, виджеты, контейнеры, canvas, темы, binding, сборка под мобильные и десктоп. 25 уроков и итоговый менеджер задач.

  • Создавать окна и интерфейсы из виджетов
  • Управлять компоновкой: Box, Grid, Form
  • Рисовать примитивами canvas и строить свои виджеты
  • Связывать данные через binding