Структура Go проектов: от простого к сложному
Как пользоваться руководством
Если вы новичок в Go, не пытайтесь освоить всё сразу. Начните с основ:
Сначала прочитайте:
- Введение и Почему структура проекта важна?
- Эволюция структуры проекта - первые два этапа
Когда начнёте писать код:
- Эволюция структуры проекта - этап 3 с
/internal
- Ключевые директории - основные концепции
Остальное изучайте по мере необходимости - не нужно запоминать все паттерны заранее.
Введение
Структура проекта - это одна из тех вещей, которые кажутся простыми на первый взгляд, но становятся источником головной боли по мере роста вашего приложения. В отличие от многих языков, Go предоставляет большую свободу в организации кода, что может быть как благословением, так и проклятием. Это руководство поможет вам понять, как правильно организовать Go проект, избежать типичных ошибок и создать структуру, которая будет легко поддерживаться в долгосрочной перспективе.
Почему структура проекта важна?
Проблемы неорганизованного кода
Представьте, что вы приходите на новую работу и вам дают проект со следующей структурой:
my-project/
├── main.go // 800 строк кода
├── stuff.go // Разные функции
├── helpers.go // Еще больше разных функций
├── database_things.go // Работа с базой данных
├── api_handlers.go // HTTP обработчики
├── models.go // Структуры данных
├── utils.go // "Полезные" функции
└── other_stuff.go // Непонятно что
Что не так с этой структурой:
- Отсутствие логической группировки: Код, связанный с одной функциональностью, разбросан по разным файлам
- Неясные границы ответственности: Нельзя точно сказать, какая именно логика реализована в данном файле
- Сложность навигации: Найти нужный код становится квестом
- Проблемы с тестированием: Трудно писать изолированные тесты
- Конфликты при работе в команде: Разработчики постоянно модифицируют одни и те же файлы
Преимущества хорошей структуры
Правильно организованный проект дает следующие преимущества:
- Быстрое понимание архитектуры: Новые разработчики могут быстро разобраться в проекте
- Легкость поддержки: Изменения в одной области не влияют на другие
- Упрощенное тестирование: Каждый пакет можно тестировать независимо
- Масштабируемость: Проект может расти без потери управляемости
- Переиспользование кода: Четкие границы пакетов упрощают повторное использование
Эволюция структуры проекта
Этап 1: Простой исполняемый файл
Для небольших проектов и прототипов начните максимально просто:
calculator/
├── go.mod
├── main.go
└── README.md
Когда использовать: CLI утилиты, простые скрипты, прототипы
Пример содержимого:
// main.go package main import "fmt" func main() { result := add(5, 3) fmt.Printf("Результат: %d\n", result) } func add(a, b int) int { return a + b }
Этап 2: Несколько файлов, один пакет
Когда main.go становится слишком большим, разделите код по файлам:
calculator/
├── go.mod
├── main.go // package main, точка входа
├── operations.go // package main, математические операции
├── input.go // package main, ввод данных
└── output.go // package main, вывод результатов
Когда использовать: Проекты до 1000 строк кода
Этап 3: Введение внутренних пакетов
Когда различные части кода имеют четкие границы ответственности:
calculator/
├── go.mod
├── main.go
├── internal/
│ ├── calculator/
│ │ ├── operations.go
│ │ └── operations_test.go
│ ├── parser/
│ │ ├── parser.go
│ │ └── parser_test.go
│ └── formatter/
│ ├── formatter.go
│ └── formatter_test.go
└── README.md
Когда использовать: Проекты с четкими функциональными областями
Ключевые директории и их назначение
/cmd
- Исполняемые файлы
Директория /cmd
содержит точки входа для ваших приложений. Каждое приложение должно иметь свою поддиректорию.
project/
├── cmd/
│ ├── server/ // Веб-сервер
│ │ └── main.go
│ ├── worker/ // Фоновые задачи
│ │ └── main.go
│ └── migration/ // Миграции базы данных
│ └── main.go
└── ...
Мотивация и философия /cmd
Зачем нужна директория /cmd
?
Go изначально проектировался для создания как библиотек, так и исполняемых файлов. Проблема возникла когда разработчики начали создавать проекты, которые содержат и то, и другое:
// Проблема: что делает этот проект?
my-project/
├── main.go // Исполняемый файл?
├── server.go // Или это библиотека?
├── client.go // А это что?
└── utils.go
Историческая проблема: В ранних Go проектах не было четкого разделения между "приложением" и "библиотекой". Когда кто-то делал go get github.com/username/project
, неясно было что получится - исполняемый файл или импортируемый пакет.
Решение через /cmd
: Четкое разделение намерений
project/
├── cmd/ // "Это приложения"
│ ├── server/ // Конкретное приложение
│ └── cli/ // Другое приложение
├── internal/ // "Это внутренняя логика"
└── pkg/ // "Это публичная библиотека"
Почему именно cmd
? Название произошло от Unix-традиции, где исполняемые команды хранятся в директориях /bin
, /usr/bin
, /usr/local/bin
. В контексте Go проекта cmd
означает "commands" - команды, которые можно выполнить.
Принципы использования:
- Имя директории = имя исполняемого файла
- Минимальный код в main.go (импорт и вызов)
- Не используйте
/cmd
для проектов с одним исполняемым файлом
Когда НЕ нужна /cmd
: Если у вас один исполняемый файл и нет планов на библиотеки, main.go
в корне проекта - правильный выбор.
Пример структуры main.go:
// cmd/server/main.go package main import ( "log" "github.com/myorg/myproject/internal/server" ) func main() { if err := server.Run(); err != nil { log.Fatal(err) } }
/internal
- Приватные пакеты
Важно: /internal
- это не просто соглашение, а встроенная особенность Go. Код в /internal
может импортироваться только пакетами из того же репозитория.
project/
├── internal/
│ ├── auth/ // Аутентификация
│ │ ├── auth.go
│ │ ├── jwt.go
│ │ └── auth_test.go
│ ├── database/ // Работа с БД
│ │ ├── postgres.go
│ │ └── migrations.go
│ └── api/ // HTTP API
│ ├── handlers.go
│ ├── middleware.go
│ └── routes.go
└── ...
Мотивация и философия /internal
Почему Go команда создала механизм internal
?
До появления /internal
в Go 1.4, все пакеты в проекте были публичными. Это создавало серьезные проблемы:
// Проблема: все было публично доступно github.com/company/project/ ├── main.go ├── auth.go // Любой может импортировать! ├── secret_algo.go // Секретный алгоритм тоже публичен! └── database_helpers.go // Внутренние хелперы доступны всем!
Реальная проблема в production: Разработчики других проектов начинали использовать ваши внутренние функции, думая что это стабильный API. Когда вы меняли внутреннюю логику - ломались чужие проекты.
Пример проблемы:
// Ваш проект v1.0 package auth func validateToken(token string) bool { ... } // Внутренняя функция // Кто-то в другом проекте: import "github.com/yourcompany/project/auth" // Использует вашу внутреннюю функцию if auth.validateToken(userToken) { ... } // Вы в v1.1 переименовываете функцию -> чужой код ломается!
Решение через internal
: Компилятор Go запрещает импорт пакетов из internal
извне модуля.
// Это НЕ скомпилируется, если вызывается из другого модуля: import "github.com/company/project/internal/auth" // ОШИБКА!
Как работает механизм: Go compiler проверяет путь импорта. Если он содержит /internal/
, то импорт разрешен только для пакетов, которые находятся в том же поддереве до первого internal
.
github.com/company/project/
├── cmd/server/main.go ✅ Может импортировать internal
├── pkg/api/client.go ✅ Может импортировать internal
├── internal/auth/auth.go
└── docs/
github.com/other/project/
└── main.go ❌ НЕ может импортировать internal
Философия: Четкое разделение между "публичным контрактом" и "деталями реализации".
Когда использовать:
- Код, который не должен импортироваться извне
- Бизнес-логика приложения
- Внутренние утилиты и хелперы
- Экспериментальные API, которые могут измениться
Когда НЕ стоит использовать:
- Маленькие проекты (< 500 строк)
- Код, который может быть полезен другим проектам
- Стабильные утилиты, которые можно вынести в отдельную библиотеку
/pkg
- Публичные библиотеки
Осторожно: Многие разработчики злоупотребляют /pkg
. Используйте только для кода, который действительно должен быть публичным API.
project/
├── pkg/
│ ├── logger/ // Переиспользуемый логгер
│ │ ├── logger.go
│ │ └── logger_test.go
│ └── validator/ // Валидация данных
│ ├── validator.go
│ └── rules.go
└── ...
Мотивация и философия /pkg
(и почему это спорно)
Из всех соглашений о структуре, директория /pkg
является источником наибольшей путаницы и споров. В большинстве случаев она вам не нужна.
Откуда появилась /pkg
?
Эта директория не является официальной рекомендацией Go. Она возникла из-за исторических причин и влияния других экосистем, но со временем её стали использовать неправильно.
Основная идея /pkg
: это директория для кода, который могут использовать другие проекты. То есть, это публичная библиотека, которую вы предоставляете внешнему миру.
Давайте рассмотрим на конкретных примерах, когда это хорошо, а когда — плохо.
❌ Когда /pkg
— это плохая идея
Многие разработчики ошибочно используют /pkg
как общую папку для всего кода, который не является main.go
.
Пример НЕПРАВИЛЬНОЙ структуры:
my-web-app/
├── cmd/
│ └── main.go
├── internal/
│ └── database/
│ └── db.go
├── pkg/ // 👈 НЕПРАВИЛЬНОЕ ИСПОЛЬЗОВАНИЕ
│ ├── handlers/ // Обработчики только для этого приложения
│ │ └── user.go
│ ├── models/ // Модели только для этого приложения
│ │ └── user.go
│ └── utils/ // Утилиты только для этого приложения
│ └── helpers.go
└── go.mod
Почему это плохо?
- Логическая ошибка: Код в
handlers
иmodels
предназначен только дляmy-web-app
. Никакой другой проект не будет его импортировать. Значит, по определению, это не "публичный пакет". - Лишняя вложенность: Пути импорта становятся длиннее без всякой на то причины.
import "my-web-app/pkg/handlers"
- Вместо простого:
import "my-web-app/handlers"
- Путаница: Новый разработчик, увидев
/pkg
, может подумать, что этот код используется где-то ещё. Это усложняет рефакторинг, так как создается ложное ощущение "публичного API".
Как правильно для этого случая?
Если код не предназначен для импорта другими проектами, его место — в /internal
или в корне проекта (если вы планируете его переиспользовать в рамках одного проекта).
Правильная структура для примера выше:
my-web-app/
├── cmd/
│ └── main.go
├── internal/
│ ├── handlers/ // ✅ Вся логика внутри
│ │ └── user.go
│ ├── models/
│ │ └── user.go
│ └── database/
│ └── db.go
└── go.mod
✅ Когда /pkg
может быть оправдана
/pkg
имеет смысл только тогда, когда ваш репозиторий целенаправленно предоставляет код как библиотеку для внешних потребителей.
Пример ПРАВИЛЬНОЙ структуры:
Представьте, что вы — компания, у которой есть несколько микросервисов. Вы хотите иметь общие библиотеки для логирования, метрик и работы с вашим внутренним API.
company-libraries/
├── pkg/
│ ├── logger/ // ✅ Публичный, стабильный логгер
│ │ ├── logger.go // Используется сервисами A, B, C
│ │ └── logger_test.go
│ ├── metrics/ // ✅ Публичная библиотека метрик
│ │ └── prometheus.go // Используется сервисами A, B, C
│ └── api-client/ // ✅ Клиент для внутреннего API
│ └── client.go
├── internal/
│ └── transport/ // Внутренняя логика для api-client,
│ └── http.go // которую мы не хотим показывать
└── go.mod
Почему это хорошо?
- Четкое намерение: Структура ясно говорит: "Этот репозиторий предоставляет три библиотеки:
logger
,metrics
иapi-client
". - Стабильность: Код в
/pkg
воспринимается как публичный API. Это означает, что вы не должны вносить в него изменения, которые сломают обратную совместимость. - Разделение: Внутренняя логика, например, транспортный уровень для
api-client
, скрыта в/internal
и недоступна для импорта извне.
Проверочный список: Нужна ли мне /pkg
?
Прежде чем создавать директорию /pkg
, задайте себе эти вопросы:
- Будет ли код в этой папке импортироваться другими Go модулями (репозиториями)?
- Если нет -> не используйте
/pkg
. Поместите код в/internal
.
- Если нет -> не используйте
- Является ли этот код частью публичного API, который я обязуюсь поддерживать?
- Если нет -> не используйте
/pkg
.
- Если нет -> не используйте
- Упрощает ли
/pkg
структуру или просто добавляет еще один уровень вложенности?- Если добавляет вложенность без пользы -> не используйте
/pkg
.
- Если добавляет вложенность без пользы -> не используйте
Золотое правило: Начинайте без /pkg
. Помещайте весь приватный код вашего приложения в /internal
, сгруппировав его по функциональности. Вы всегда сможете добавить /pkg
позже, если для этого появятся веские причины.
Другие полезные директории
project/
├── api/ // OpenAPI/Swagger спецификации
│ └── v1/
│ └── openapi.yaml
├── configs/ // Конфигурационные файлы
│ ├── development.yaml
│ └── production.yaml
├── scripts/ // Скрипты сборки и развертывания
│ ├── build.sh
│ └── deploy.sh
├── docs/ // Документация
│ └── architecture.md
└── test/ // Интеграционные тесты
└── integration/
└── api_test.go
Принципы организации пакетов
1. Группировка по функциональности, а не по типу
❌ Плохо (группировка по типу):
project/
├── models/ // Все структуры данных
├── handlers/ // Все HTTP обработчики
├── services/ // Вся бизнес-логика
└── repositories/ // Весь код работы с БД
✅ Хорошо (группировка по функциональности):
project/
├── internal/
│ ├── user/ // Все, что связано с пользователями
│ │ ├── model.go
│ │ ├── handler.go
│ │ ├── service.go
│ │ └── repository.go
│ └── order/ // Все, что связано с заказами
│ ├── model.go
│ ├── handler.go
│ ├── service.go
│ └── repository.go
Почему группировка по функциональности лучше?
Проблема группировки по типу:
Когда вы группируете код по техническим ролям, для добавления новой функции приходится модифицировать файлы в разных директориях:
// Добавляем функцию "управление продуктами"
// Нужно изменить файлы в 4 разных местах:
models/product.go // +Product struct
handlers/product.go // +ProductHandler
services/product.go // +ProductService
repositories/product.go // +ProductRepository
// При любом изменении логики продуктов - снова 4 файла!
Проблемы в команде:
- При работе над одной функцией разработчики изменяют файлы в разных директориях
- Сложно найти все части одной функциональности
- Конфликты при слияниях веток (merge conflicts)
- Неясно, где искать код для конкретной фичи
Преимущества группировки по функциональности:
// Добавляем функцию "управление продуктами"
// Все в одном месте:
internal/product/
├── model.go // Product struct
├── handler.go // HTTP handlers
├── service.go // Business logic
├── repository.go // Database operations
└── product_test.go // Все тесты здесь же
Преимущества:
- Связность: Связанный код находится рядом
- Понятность: Сразу видно, какие функции есть в системе
- Изолированность: Изменения в одной функции не влияют на другие
- Командная работа: Разные разработчики могут работать с разными функциями без конфликтов
Практический пример разницы:
// Плохо: нужно импортировать из разных мест import ( "project/models" "project/services" "project/repositories" ) func CreateUser(userData UserRequest) { user := models.User{...} // Откуда эта структура? err := repositories.SaveUser(user) // А где этот код? services.SendWelcomeEmail(user) // И этот тоже где-то далеко } // Хорошо: все понятно из импорта import "project/internal/user" func CreateUser(userData UserRequest) { // Все функции user находятся в одном пакете user := user.New(userData) err := user.Save() user.SendWelcomeEmail() }
Философия Go: Пакеты должны представлять функциональные области, а не технические слои.
2. Избегайте циклических зависимостей
Go запрещает циклические импорты. Проектируйте архитектуру как направленный ациклический граф (DAG).
// Правильная иерархия зависимостей
main.go
↓
internal/api/
↓
internal/service/
↓
internal/repository/
↓
internal/database/
Почему Go запрещает циклические зависимости?
Техническая причина: Компилятор Go компилирует пакеты в определенном порядке. При циклических зависимостях невозможно определить, какой пакет компилировать первым.
// Это НЕ скомпилируется: // package a import "project/b" func DoA() { b.DoB() } // package b import "project/a" func DoB() { a.DoA() } // Компилятор: "Что компилировать сначала - a или b?"
Философская причина: Циклические зависимости указывают на плохую архитектуру. Если два пакета зависят друг от друга, возможно, они должны быть одним пакетом или нужна реорганизация.
Типичные проблемы и решения:
Проблема 1: Взаимные вызовы
// user/user.go import "project/order" func (u *User) GetOrders() []order.Order { ... } // order/order.go import "project/user" func (o *Order) GetUser() user.User { ... } // ❌ Цикл: user → order → user
Решение 1: Интерфейсы
// order/order.go type UserGetter interface { GetUser(id string) User } type Order struct { userGetter UserGetter // Не импортируем user пакет! } // main.go - связываем зависимости userService := user.NewService() orderService := order.NewService(userService)
Проблема 2: Общие типы
// user/user.go import "project/order" // Нужен Order тип // order/order.go import "project/user" // Нужен User тип // ❌ Цикл из-за общих типов
Решение 2: Общий пакет типов
// types/types.go (или models/models.go) type User struct { ... } type Order struct { ... } // user/user.go import "project/types" func ProcessUser(u types.User) { ... } // order/order.go import "project/types" func ProcessOrder(o types.Order) { ... }
Решение 3: Dependency Inversion
// Вместо прямых зависимостей используем интерфейсы // service/user.go type UserService struct { orderRepo OrderRepository // Интерфейс, не конкретный тип } type OrderRepository interface { GetOrdersByUser(userID string) []Order } // repository/order.go - реализует интерфейс type PostgresOrderRepo struct { ... } func (r *PostgresOrderRepo) GetOrdersByUser(userID string) []Order { ... }
Инструменты для обнаружения циклов:
# Проверка зависимостей go mod graph | grep -E "(internal|pkg)" # Визуализация (если установлен graphviz) go mod graph | dot -T svg -o deps.svg
3. Принцип единственной ответственности для пакетов
Каждый пакет должен иметь одну четкую ответственность:
// internal/email/ - только для работы с email package email type Sender interface { Send(to, subject, body string) error } type SMTPSender struct { host string port int } func (s *SMTPSender) Send(to, subject, body string) error { // Реализация отправки email return nil }
4. Интерфейсы определяйте там, где используете
// internal/notification/service.go package notification // Определяем интерфейс там, где используем type EmailSender interface { Send(to, subject, body string) error } type Service struct { emailSender EmailSender } func NewService(emailSender EmailSender) *Service { return &Service{emailSender: emailSender} }
Практические примеры структур
Веб-приложение
ecommerce-api/
├── cmd/
│ ├── api/
│ │ └── main.go
│ └── worker/
│ └── main.go
├── internal/
│ ├── api/
│ │ ├── middleware/
│ │ ├── routes/
│ │ └── handlers/
│ ├── domain/
│ │ ├── user/
│ │ ├── product/
│ │ └── order/
│ ├── infrastructure/
│ │ ├── database/
│ │ ├── cache/
│ │ └── queue/
│ └── config/
├── api/
│ └── v1/
│ └── openapi.yaml
├── migrations/
├── configs/
├── go.mod
└── go.sum
CLI утилита
git-analyzer/
├── cmd/
│ ├── analyze/
│ │ └── main.go
│ └── report/
│ └── main.go
├── internal/
│ ├── git/
│ │ ├── parser.go
│ │ └── repository.go
│ ├── analyzer/
│ │ ├── complexity.go
│ │ └── metrics.go
│ └── reporter/
│ ├── console.go
│ └── json.go
├── testdata/
├── go.mod
└── go.sum
Библиотека
math-utils/
├── calculator/
│ ├── basic.go
│ ├── advanced.go
│ └── calculator_test.go
├── geometry/
│ ├── shapes.go
│ ├── area.go
│ └── geometry_test.go
├── statistics/
│ ├── mean.go
│ ├── median.go
│ └── statistics_test.go
├── examples/
│ └── main.go
├── go.mod
└── go.sum
Типичные ошибки и как их избежать
1. Преждевременная оптимизация структуры
❌ Ошибка: Создание сложной структуры с самого начала
new-project/
├── cmd/
├── internal/
│ ├── domain/
│ ├── infrastructure/
│ ├── application/
│ └── interfaces/
├── pkg/
└── api/
✅ Решение: Начинайте просто и развивайте по мере роста
2. Злоупотребление пакетом utils
❌ Ошибка: Создание пакета-свалки
// internal/utils/utils.go package utils func FormatDate(date time.Time) string { ... } func HashPassword(password string) string { ... } func ValidateEmail(email string) bool { ... } func CalculateDistance(lat1, lon1, lat2, lon2 float64) float64 { ... }
✅ Решение: Создавайте специализированные пакеты
internal/
├── datetime/
│ └── formatter.go
├── auth/
│ └── password.go
├── validation/
│ └── email.go
└── geo/
└── distance.go
3. Слишком глубокая вложенность
❌ Ошибка: Чрезмерная иерархия
internal/domain/entities/user/model/struct/definition/user.go
✅ Решение: Максимум 3-4 уровня вложенности
internal/user/model.go
4. Неправильное использование internal
❌ Ошибка: Использование internal
для маленьких проектов
simple-cli/
├── internal/ // Не нужно для CLI из 200 строк
│ └── logic/
│ └── process.go
└── main.go
✅ Решение: Для простых проектов достаточно одного пакета
simple-cli/
├── main.go
├── process.go
└── process_test.go
Рекомендации для разных типов проектов
Микросервисы
payment-service/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── api/ // HTTP API
│ ├── grpc/ // gRPC API
│ ├── payment/ // Бизнес-логика
│ ├── repository/ // Работа с БД
│ └── config/ // Конфигурация
├── proto/ // gRPC определения
├── migrations/
└── docker/
Монолитные приложения
ecommerce-monolith/
├── cmd/
│ ├── web/ // Веб-приложение
│ ├── api/ // REST API
│ └── admin/ // Административный интерфейс
├── internal/
│ ├── user/ // Управление пользователями
│ ├── product/ // Управление товарами
│ ├── order/ // Управление заказами
│ ├── payment/ // Обработка платежей
│ ├── shared/ // Общие компоненты
│ └── infrastructure/
├── web/
│ ├── static/
│ └── templates/
└── migrations/
CLI утилиты
deployment-tool/
├── cmd/
│ ├── deploy/
│ ├── rollback/
│ └── status/
├── internal/
│ ├── kubernetes/
│ ├── docker/
│ ├── config/
│ └── logger/
├── configs/
│ └── templates/
└── docs/
Инструменты и автоматизация
Генерация структуры проекта
Используйте инструменты для автоматического создания структуры:
# Создание стандартной структуры go mod init github.com/username/project mkdir -p cmd/server internal pkg
Линтеры для проверки структуры
# .golangci.yml linters: enable: - goimports # Проверка импортов - unused # Неиспользуемый код - gosec # Безопасность - ineffassign # Неэффективные присвоения
Автоматическая проверка зависимостей
# Проверка циклических зависимостей go mod graph | grep -E "(internal|pkg)" | sort | uniq
Миграция существующих проектов
Поэтапный подход
- Анализ текущей структуры: Определите функциональные области
- Создание новой структуры: Начните с основных директорий
- Постепенный перенос: Переносите код по одному пакету
- Обновление импортов: Используйте инструменты автоматического рефакторинга
- Тестирование: Убедитесь, что все работает корректно
Пример миграции
До:
old-project/
├── main.go // 500 строк
├── handlers.go // 300 строк
├── database.go // 200 строк
└── utils.go // 150 строк
После:
new-project/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── api/
│ │ └── handlers.go
│ ├── database/
│ │ └── postgres.go
│ └── utils/
│ └── helpers.go
└── go.mod
Заключение
Хорошая структура проекта экономит время и нервы в долгосрочной перспективе. Но не усложняйте всё с самого начала.
Основные принципы:
- Начинайте с
main.go
- добавляйте структуру по мере роста проекта - Группируйте код по функциональности - user, product, order (а не models, handlers, services)
- Используйте
/internal
осознанно - не для каждого маленького проекта - Избегайте глубокой вложенности - больше 3-4 уровней усложняет навигацию
- Проверяйте себя простым вопросом: "Где найти код для функции X?" Если ответ не очевиден - пересмотрите структуру.
Краткая шпаргалка:
Размер проекта | Что использовать |
---|---|
< 200 строк | main.go + README.md |
200-1000 строк | main.go + несколько .go файлов |
1000+ строк | internal/ пакеты по функциональности |
Несколько приложений | /cmd + /internal |
Библиотека для других | Пакеты в корне, без /internal |
Помните: если структура мешает работе, а не помогает - значит, пора её упростить.
Полезные ресурсы
- Официальная документация по модулям Go
- Effective Go
- Go Code Review Comments
- Standard Go Project Layout (спорное, но популярное)