Project Layout в Go
Зачем знать: В Go нет единого «официального» layout — но есть устоявшиеся практики. От правильной структуры зависит, сможет ли команда масштабироваться, найдёт ли новичок код за 5 минут или будет ползать по 50 пакетам. На middle-уровне ждут понимания, ЧТО, ЗАЧЕМ и КОГДА.
Содержание
Заголовок раздела «Содержание»- Базовая концепция
- В Go идиоматично
- Gotchas
- Best practices
- Вопросы на собесе
- Practice
- Источники
1. Базовая концепция
Заголовок раздела «1. Базовая концепция»1.1 Нет официального стандарта
Заголовок раздела «1.1 Нет официального стандарта»Команда Go никогда не публиковала «официальный» layout. Это вызывает споры. Самое цитируемое:
- golang-standards/project-layout — неофициальный гайд, де-факто стандарт у многих команд (хотя его критикуют).
- Bill Kennedy / Ardan Labs — package-oriented design.
- Mat Ryer — flat structure для микросервиса.
- Google Go Style Guide — конкретные правила имён.
Каждый из этих подходов работает. Главное — внутренняя консистентность в проекте.
1.2 golang-standards/project-layout
Заголовок раздела «1.2 golang-standards/project-layout»Самая известная структура:
my-project/├── cmd/ # main-пакеты приложений│ └── api/│ └── main.go├── internal/ # приватный код (compiler-enforced)│ ├── app/│ ├── pkg/│ └── ...├── pkg/ # публичные библиотеки (если есть)├── api/ # OpenAPI/proto/gRPC файлы├── web/ # static, шаблоны├── configs/ # конфиги├── deployments/ # Dockerfile, k8s manifests├── scripts/ # bash-утилиты├── test/ # интеграционные тесты, fixtures├── docs/ # документация├── examples/ # примеры использования (для библиотек)├── tools/ # tools.go (build tools)├── vendor/ # vendored deps (опционально)├── go.mod├── go.sum└── MakefileКритика: многие считают, что это «cargo cult» — копирование без понимания. Russ Cox (один из авторов Go) писал что это «not an official standard». Команды используют это как отправную точку, упрощая.
1.3 internal/ — keyword компилятора
Заголовок раздела «1.3 internal/ — keyword компилятора»Это единственный path, который Go-компилятор обрабатывает специально.
github.com/foo/bar/├── internal/secret/│ └── magic.go // package secret└── public/ └── public.go // package public, может импортить internal/secretgithub.com/baz/qux/└── main.go // НЕ может импортить github.com/foo/bar/internal/secret // compile error: use of internal package not allowedПравило: пакет в <repo>/internal/X доступен только в <repo>/.... Если репозиторий ниже корня — <root>/internal/X доступен только в поддереве <root>/....
Используется как enforce приватности: если ты пишешь сервис — клади ВСЁ в internal/. Тогда никто извне не зацепится за твою внутреннюю структуру.
1.4 cmd/ — entry points
Заголовок раздела «1.4 cmd/ — entry points»Каждый main-пакет — в своей подпапке внутри cmd/. Имя папки = имя binary.
cmd/├── api/main.go // go build -o api ./cmd/api├── worker/main.go // go build -o worker ./cmd/worker└── migrator/main.go // go build -o migrator ./cmd/migratormain.go — тонкий, только composition root. Никакой бизнес-логики.
1.5 pkg/ — публичные пакеты
Заголовок раздела «1.5 pkg/ — публичные пакеты»Идея: всё, что должно быть импортируемо снаружи, кладётся в pkg/. Остальное — в internal/.
Критика pkg/:
- Если репозиторий — приложение (не библиотека),
pkg/не нужен вообще. - Большинство публичных пакетов кладутся в корень репо (как
pkg/errorsот Dave Cheney илиpgx). - Это лишний уровень вложенности.
Rakyll и многие другие советуют не использовать pkg/ в простых проектах.
1.6 Bill Kennedy: domain-driven layout
Заголовок раздела «1.6 Bill Kennedy: domain-driven layout»Подход Ardan Labs:
service/├── app/ // application layer│ ├── services/│ │ └── sales-api/ // одно приложение│ │ └── main.go│ └── tooling/│ └── admin/├── business/ // business layer│ ├── core/ // domain types (entity, aggregate)│ │ ├── order/│ │ ├── user/│ │ └── product/│ ├── data/ // persistence│ │ └── dbschema/│ └── web/ // веб-логика (auth, errors, validation)├── foundation/ // re-usable infrastructure│ ├── docker/│ ├── logger/│ └── web/├── vendor/└── go.modИдея: 3 слоя — application, business, foundation. foundation — низкоуровневое (logger, http kit), business — домен, application — entry point.
1.7 Hexagonal layout
Заголовок раздела «1.7 Hexagonal layout»Для команд, делающих Clean Arch:
service/├── cmd/api/main.go├── internal/│ ├── domain/ // entities│ ├── ports/ // interfaces (in/out)│ ├── adapters/│ │ ├── primary/ // HTTP/gRPC handlers│ │ └── secondary/ // DB, Kafka│ └── application/ // use cases└── go.mod1.8 Flat layout (Mat Ryer)
Заголовок раздела «1.8 Flat layout (Mat Ryer)»Для маленького микросервиса:
service/├── main.go├── server.go├── routes.go├── handlers.go├── auth.go├── db.go├── tests/└── go.modОдин пакет, плоская структура. Хорошо работает, пока сервис маленький.
1.9 Mono-repo vs Poly-repo
Заголовок раздела «1.9 Mono-repo vs Poly-repo»| Mono-repo | Poly-repo | |
|---|---|---|
| Один репо со всеми сервисами | каждый сервис — свой репо | |
| Pros | shared code, atomic changes, проще CI | автономия команд, фокус |
| Cons | большие репо, сложный CI | дублирование, версионирование сложно |
| Tools | Bazel, go workspaces | go modules |
Для Go в mono-repo популярны:
- go workspaces (1.18+) — несколько модулей в одном репо;
- Bazel — для крупных компаний (Google, Uber);
- Один go.mod в корне +
internal/для модулей.
1.10 go workspaces
Заголовок раздела «1.10 go workspaces»go.work — workspace file, объединяет несколько модулей для локальной разработки.
my-monorepo/├── go.work├── services/│ ├── orders/│ │ ├── go.mod // module github.com/co/orders│ │ └── main.go│ └── payments/│ ├── go.mod // module github.com/co/payments│ └── main.go└── pkg/ └── logger/ ├── go.mod // module github.com/co/logger └── logger.gogo.work:
go 1.22
use ( ./services/orders ./services/payments ./pkg/logger)Преимущества:
- Локальные изменения в
loggerсразу видны вorders/payments, безreplace-директив. - Каждый модуль независимо релизится.
go.workне коммитят (в .gitignore) — это локальный артефакт. Хотя некоторые команды коммитят для unified dev experience.
2. В Go идиоматично
Заголовок раздела «2. В Go идиоматично»2.1 Package naming
Заголовок раздела «2.1 Package naming»Правила (из effective Go):
- short —
time,http,log,os; - lowercase —
bytes, неBytes; - no underscores —
httputil, неhttp_util; - no camelCase —
httputil, неhttpUtil; - singular —
user, неusers; - descriptive —
order, неo.
Избегайте generic-имён:
- ❌
util,common,helper,base,lib,tools— ничего не сообщают. - ✅
httputil,slicesx,idgen,pricing.
// ПЛОХО:// package util// func Pluralize(s string) string {...}util.Pluralize("user")
// ХОРОШО:// package stringx// func Pluralize(s string) string {...}stringx.Pluralize("user")2.2 Package design principles
Заголовок раздела «2.2 Package design principles»Cohesion
Заголовок раздела «Cohesion»Пакет — одна цель. Если в пакете 50 несвязанных функций — разделите.
// ПЛОХО: package utilutil.FormatTime(...)util.ParseJSON(...)util.HashPassword(...)util.GenerateUUID(...)
// ХОРОШО:timeutil.Format(...)jsonutil.Parse(...)crypto.HashPassword(...)uuid.New()Minimal API surface
Заголовок раздела «Minimal API surface»Экспортируйте только то, что нужно. Чем меньше публичных типов, тем легче эволюция.
// ПЛОХО: всё publictype Server struct { Logger *slog.Logger DB *sql.DB Cache *redis.Client Internal internalState}
// ХОРОШО: public — только то, что нужно вызыватьtype Server struct { logger *slog.Logger db *sql.DB cache *redis.Client}
func (s *Server) Run() error { ... }func (s *Server) Shutdown(ctx context.Context) error { ... }Cycle detection
Заголовок раздела «Cycle detection»Go запрещает циклические импорты (compile error). Если package a импортирует package b, b НЕ может импортировать a.
Типичные решения:
- Выделить общий интерфейс/тип в отдельный пакет (
api,model). - Использовать функциональные параметры/колбэки.
- Объединить пакеты, если они слишком связаны.
// Цикл:// package order: import "user"// package user: import "order"
// Решение 1: общий пакет// package model: type UserID, type OrderID// package order, user — import model
// Решение 2: интерфейсы// package order: type UserService interface { GetByID(...) }// package user реализует UserService, но не импортит order2.3 Tests location
Заголовок раздела «2.3 Tests location»Тесты лежат рядом с кодом:
package order/├── order.go├── order_test.go // package order (white-box)└── order_external_test.go // package order_test (black-box)White-box vs Black-box
Заголовок раздела «White-box vs Black-box»package orderв_test.go— видит приватные функции (white-box).package order_test— только public API (black-box). Полезно для библиотек.
package ordertype Order struct { ... }func (o *Order) calculate() int { ... }
// order_test.go: white-boxpackage orderimport "testing"func TestCalculate(t *testing.T) { o := &Order{} o.calculate() // видит приватный}
// order_external_test.go: black-boxpackage order_testimport ( "testing" "myrepo/order")func TestCalculate(t *testing.T) { o := order.New() o.Total() // только public API}2.4 testdata/
Заголовок раздела «2.4 testdata/»Папка testdata/ — особая. Go-tools игнорируют её при сборке.
order/├── order.go├── order_test.go└── testdata/ ├── valid_order.json └── invalid_order.jsonfunc TestParseOrder(t *testing.T) { data, _ := os.ReadFile("testdata/valid_order.json") o, err := ParseOrder(data) // ...}2.5 vendor/
Заголовок раздела «2.5 vendor/»vendor/ — папка с локальной копией зависимостей. До go modules это был основной механизм.
Когда нужен:
- Offline builds (CI без интернета);
- Воспроизводимость (хотя go.sum + module proxy решают это);
- Аудит зависимостей (можно посмотреть исходники);
- Регуляторика в банковской сфере.
Команды:
go mod vendor # создать vendor/go build # использует vendor/, если естьgo build -mod=mod # игнорирует vendor/Современные best practices: не vendor, если нет специальной причины.
2.6 go.mod в корне
Заголовок раздела «2.6 go.mod в корне»module github.com/myorg/myservice
go 1.22
require ( github.com/jackc/pgx/v5 v5.5.0 github.com/google/uuid v1.5.0)
require ( // indirect github.com/jackc/puddle/v2 v2.2.1 // indirect)Один go.mod на репо (если не workspace). Имя модуля = import path. Принято — github.com/<org>/<repo>.
2.7 Конкретные примеры layout
Заголовок раздела «2.7 Конкретные примеры layout»Микросервис (Hexagonal)
Заголовок раздела «Микросервис (Hexagonal)»order-service/├── cmd/│ └── api/main.go├── internal/│ ├── domain/ // entities│ │ ├── order.go│ │ └── money.go│ ├── usecase/ // use cases│ │ ├── create_order.go│ │ ├── create_order_test.go│ │ └── ports.go // interfaces│ ├── adapter/│ │ ├── postgres/│ │ │ ├── order_repo.go│ │ │ └── order_repo_test.go│ │ ├── kafka/│ │ │ └── publisher.go│ │ └── http/│ │ ├── order_handler.go│ │ └── middleware.go│ ├── config/│ │ └── config.go│ └── app/│ └── app.go // composition root├── api/│ └── openapi.yaml├── migrations/│ ├── 001_create_orders.up.sql│ └── 001_create_orders.down.sql├── deployments/│ ├── Dockerfile│ └── k8s/├── scripts/├── Makefile├── go.mod└── README.mdCLI утилита
Заголовок раздела «CLI утилита»mycli/├── main.go // если binary — единственный├── cmd/ // если несколько subcommands (cobra)│ ├── root.go│ ├── create.go│ └── list.go├── internal/│ ├── service/│ └── client/├── go.mod└── README.mdБиблиотека
Заголовок раздела «Библиотека»mylib/├── doc.go // package-level doc├── mylib.go // основные типы и API├── client.go├── options.go├── errors.go├── mylib_test.go├── examples/│ ├── basic/│ │ └── main.go│ └── advanced/│ └── main.go├── internal/ // приватные хелперы│ └── proto/├── go.mod├── LICENSE└── README.mdЗаметьте: нет cmd/, internal/X/Y/Z, pkg/ — это лишняя вложенность для библиотеки.
2.8 Tools versioning (tools.go)
Заголовок раздела «2.8 Tools versioning (tools.go)»Для CI-инструментов, которые должны быть зафиксированы в go.sum:
//go:build tools
package tools
import ( _ "github.com/golang-migrate/migrate/v4/cmd/migrate" _ "github.com/swaggo/swag/cmd/swag" _ "github.com/golangci/golangci-lint/cmd/golangci-lint")go.sum фиксирует версии, в CI:
go install github.com/golang-migrate/migrate/v4/cmd/migrateВ Go 1.24+ — go.mod tool directive (новее, рекомендуется).
2.9 Makefile
Заголовок раздела «2.9 Makefile»Стандарт для Go-проектов:
.PHONY: build test lint run docker
build: go build -o bin/api ./cmd/api
test: go test ./... -race -cover
lint: golangci-lint run
run: go run ./cmd/api
docker: docker build -t myorg/api:$(shell git rev-parse --short HEAD) .
migrate-up: migrate -database "$(DATABASE_URL)" -path migrations up
generate: go generate ./...3. Gotchas
Заголовок раздела «3. Gotchas»3.1 pkg/ everywhere
Заголовок раздела «3.1 pkg/ everywhere»Многие новички копируют pkg/ из golang-standards. В реальности, если проект — приложение, pkg/ бесполезен. Используйте internal/.
3.2 Глубокая вложенность
Заголовок раздела «3.2 Глубокая вложенность»// ПЛОХО:internal/services/order/usecase/createOrder/handler/... // 6 уровнейНе делайте глубже 3-4 уровней без причины.
3.3 Generic-имена пакетов
Заголовок раздела «3.3 Generic-имена пакетов»// ПЛОХО:import "myrepo/util"util.Format(...)util.Parse(...)
// Что делает util? Никто не знает.Лечите конкретными именами: timeutil, httputil, slicesx.
3.4 Циклические импорты
Заголовок раздела «3.4 Циклические импорты»package a: import "b" type A struct{}
package b: import "a" // CYCLE!Compile error: import cycle not allowed. Решение — общий пакет или интерфейсы.
3.5 Импорт internal/ чужого репо
Заголовок раздела «3.5 Импорт internal/ чужого репо»// в репо github.com/foo/bar:import "github.com/baz/qux/internal/secret"// compile error: use of internal package not allowedЭто by design. Если действительно нужно — копируй код (с указанием license).
3.6 Один большой пакет
Заголовок раздела «3.6 Один большой пакет»// package myservice (1000 файлов, 50000 строк)Это monolith внутри Go. Сложно навигировать, легко плодить циклы внутри пакета. Делите по доменам.
3.7 «Папка для каждой вещи»
Заголовок раздела «3.7 «Папка для каждой вещи»»internal/├── interfaces/├── types/├── consts/├── helpers/├── models/└── services/Это Java-style, не Go. В Go типы и интерфейсы живут с кодом, который их использует, а не отдельно.
3.8 Циркулярная зависимость через interface
Заголовок раздела «3.8 Циркулярная зависимость через interface»Иногда новички пытаются сделать так:
// package aimport "b"type A struct { b *B.B }
// package btype Doer interface { Do() }// Doer реализован в a, а b его используетЭто работает (нет цикла, потому что b ничего не импортит из a). Это стандартная техника: интерфейс там, где он используется.
3.9 cmd/ с бизнес-логикой
Заголовок раздела «3.9 cmd/ с бизнес-логикой»// ПЛОХО: cmd/api/main.go — 500 строк бизнес-логикиmain.go — тонкий wiring. Логика — в internal/. Cmd-папка должна содержать ровно столько кода, чтобы запустить приложение.
3.10 vendor + go.mod без причины
Заголовок раздела «3.10 vendor + go.mod без причины»vendor/ дублирует deps, увеличивает репо, требует ручной синхронизации. Не используйте, если нет конкретной причины (offline builds, аудит, регуляторика).
3.11 testdata/ внутри обычного пакета
Заголовок раздела «3.11 testdata/ внутри обычного пакета»order/├── order.go└── testdata.go // ❌ не testdatatestdata — папка, не файл. Go tools игнорируют именно папку.
3.12 Tests в отдельной папке tests/
Заголовок раздела «3.12 Tests в отдельной папке tests/»// ПЛОХО:project/├── order/│ └── order.go└── tests/ └── order_test.go // ❌ не находится рядом с кодомВ Go тесты — рядом с кодом, в том же пакете. tests/ — только для интеграционных или E2E, не unit.
3.13 internal/pkg vs internal
Заголовок раздела «3.13 internal/pkg vs internal»Иногда видишь internal/pkg/X — это смесь двух подходов. Достаточно internal/X.
3.14 go.work commit или не commit?
Заголовок раздела «3.14 go.work commit или не commit?»Точка спора. Аргументы:
- Коммитить: unified dev experience, не нужно каждому настраивать локально.
- Не коммитить: workspace может конфликтовать с CI (где multi-module билдится по-другому).
Многие команды используют go.work.example в репо, реальный go.work — в .gitignore.
4. Best practices
Заголовок раздела «4. Best practices»- Старт — flat layout. Усложняйте по мере роста.
internal/для приватной логики. Compiler enforce, защита от внешних пользователей.cmd/<name>/main.goдля каждого binary. Имя папки = имя binary.pkg/НЕ используйте, если проект — приложение, а не библиотека.- Имена пакетов short, lowercase, singular. Без
util/common/helper. - Один пакет — одна цель (high cohesion).
- Минимальный экспорт. Сначала private, экспорт — по необходимости.
- Интерфейс там, где используется. Не в реализации.
- Тесты рядом с кодом.
*_test.goв том же пакете. testdata/для фикстур. Go tools игнорируют.- Migrations в
migrations/в корне. - API spec (OpenAPI, .proto) в
api/. - Deployment artifacts в
deployments/(Dockerfile, k8s). - Makefile — стандарт для build/test/lint/run.
- Не плодите вложенность — 3-4 уровня max без сильной причины.
- Композицию делать в main.go (или
internal/app/). - go.work — для workspace локально, обычно
.gitignore. - Documentation в
doc.goдля package-level doc (виден в pkg.go.dev).
5. Вопросы на собесе
Заголовок раздела «5. Вопросы на собесе»-
Зачем нужна папка
internal/в Go? Это keyword компилятора: пакеты в<repo>/internal/нельзя импортить за пределами<repo>. Используется для приватной бизнес-логики. -
Чем
pkg/отличается отinternal/?pkg/— публичные пакеты, доступны откуда угодно.internal/— приватные, compile-enforced. Многие советуют не использоватьpkg/в простых проектах. -
Что положить в
cmd/? Подпапку на каждый main-binary.cmd/api/main.go,cmd/worker/main.go. Имя папки = имя binary. -
Зачем разделять
cmd/apiиcmd/worker? Чтобы был отдельный binary на роль. Деплоятся отдельно, разная конфигурация, разные ресурсы. -
Почему
util,common,helper— плохие имена пакетов? Не сообщают, что внутри. Превращаются в свалку. Используйте конкретные:httputil,slicesx. -
Что такое
testdata/? Папка с фикстурами для тестов. Go-tools (go build,go test) игнорируют её при поиске исходников. -
White-box vs black-box тесты? White-box:
package orderв_test.go, видит приватные. Black-box:package order_test, только public API. -
Можно ли импортить
pkg-x/internal/yизpkg-y? Нет, еслиpkg-yне в поддеревеpkg-x. Compile error. -
Что такое
go.workи когда нужен? Workspace file (Go 1.18+) для нескольких модулей в одном репо безreplace-директив. Удобно для локальной разработки monorepo. -
Чем mono-repo отличается от poly-repo? Mono — один репо для всех сервисов, poly — каждый сервис свой репо. Mono упрощает shared code и atomic changes, poly даёт автономию.
-
Куда положить миграции БД? В корне
migrations/, отдельная папка. Запускаются отдельным инструментом (golang-migrate, goose, atlas). -
Куда .proto/.openapi.yaml? В
api/в корне репо. Документация контракта. -
Что такое
vendor/? Папка с локальной копией зависимостей. Используется для offline builds, аудита, регуляторики. Создаётсяgo mod vendor. -
Где должен жить
Dockerfile? В корне (если один) или вdeployments/(если несколько image). Многие команды кладут в корень для простоты. -
Можно ли в Go циклически импортить пакеты? Нет, compile error: import cycle not allowed. Лечится разделением, общим пакетом или интерфейсом.
-
Зачем разделять test fixtures и unit-тесты? testdata/ — данные для тестов. Тесты — рядом с кодом, в том же пакете.
-
Что такое
doc.go? Файл с package-level документацией. Содержит только// package docкомментарий иpackage Xdeclaration. Виден на pkg.go.dev. -
Куда положить общую логику логирования? В отдельный пакет, например
internal/logger/илиpkg/logger/(если переиспользуется между сервисами). -
Когда оправдан
pkg/? Если репозиторий — библиотека, или содержит публичные SDK для других команд. -
Почему main.go должен быть тонким? Чтобы бизнес-логика была testable и переиспользуема. main.go — только wiring, остальное в
internal/.
6. Practice
Заголовок раздела «6. Practice»-
Возьмите свой текущий проект и проверьте: есть ли
internal/? Есть лиpkg/? Нужны ли они на самом деле? Перенесите код, если что-то не так. -
Создайте микросервис со структурой:
cmd/api/main.gointernal/domain/,internal/usecase/,internal/adapter/postgres/,internal/adapter/http/api/openapi.yamlmigrations/Makefileсbuild,test,lint,run.
-
Сделайте mono-repo на go workspaces:
- 2 микросервиса (orders, payments);
- 1 shared пакет (logger);
go.workобъединяет их;- покажите, что изменение в logger мгновенно видно в обоих сервисах.
-
Найдите в каком-нибудь популярном репо (например,
kubernetes/kubernetes,cockroachdb/cockroach) — как они структурированы. Сравните с golang-standards. -
Напишите
tools.goс зависимостями:golang-migrate,swag,golangci-lint. Зафиксируйте вgo.sum.
7. Источники
Заголовок раздела «7. Источники»- golang-standards/project-layout — https://github.com/golang-standards/project-layout
- Google Go Style Guide — https://google.github.io/styleguide/go/
- Effective Go (Names) — https://go.dev/doc/effective_go#names
- Russ Cox. Не stdlib gopher — https://twitter.com/_rsc/status/1199828128876773376
- Dave Cheney. Avoid package names like base, util, or common — https://dave.cheney.net/2019/01/08/avoid-package-names-like-base-util-or-common
- Bill Kennedy. Package-Oriented Design — https://www.ardanlabs.com/blog/2017/02/package-oriented-design.html
- Mat Ryer. How I write HTTP services in Go after 13 years — https://grafana.com/blog/2024/02/09/how-i-write-http-services-in-go-after-13-years/
- Peter Bourgon. Go Best Practices, Six Years In — https://peter.bourgon.org/go-best-practices-2016/
- Go workspaces — https://go.dev/blog/get-familiar-with-workspaces