Структура проекта Go: практики и шаблоны

Структурируйте проекты на Go для масштабируемости и ясности

Содержимое страницы

Эффективная структура проекта на Go является фундаментальной для долгосрочного сопровождения, командной работы и масштабируемости. В отличие от фреймворков, которые навязывают жесткую структуру каталогов, Go предлагает гибкость — но вместе со свободой приходит ответственность за выбор паттернов, соответствующих конкретным потребностям вашего проекта.

project tree

Понимание философии Go в отношении структуры проекта

Минималистичная философия дизайна Go распространяется и на организацию проектов. Язык не предписывает конкретную структуру, доверяя разработчикам принимать обоснованные решения. Этот подход привел к тому, что сообщество разработало несколько проверенных паттернов: от простых плоских структур для небольших проектов до сложной архитектуры для корпоративных систем.

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

Когда следует использовать директорию internal/ вместо pkg/?

Директория internal/ служит определенной цели в дизайне Go: она содержит пакеты, которые не могут быть импортированы внешними проектами. Компилятор Go принудительно соблюдает это ограничение, что делает internal/ идеальным местом для частой логики приложения, бизнес-правил и утилит, не предназначенных для повторного использования за пределами вашего проекта.

Директория pkg/, с другой стороны, сигнализирует о том, что код предназначен для внешнего потребления. Размещайте код здесь только если вы создаете библиотеку или повторно используемые компоненты, которые другие должны импортировать. Многим проектам вообще не нужна pkg/ — если ваш код не используется внешними системами, хранение его в корне или в internal/ выглядит чище. При создании повторно используемых библиотек рассмотрите возможность использования Go generics, чтобы создавать типобезопасные компоненты.

Стандартная структура проекта Go

Наиболее признанным паттерном является golang-standards/project-layout, хотя важно отметить, что это не официальный стандарт. Вот как выглядит типичная структура:

myproject/
├── cmd/
│   ├── api/
│   │   └── main.go
│   └── worker/
│       └── main.go
├── internal/
│   ├── auth/
│   ├── storage/
│   └── transport/
├── pkg/
│   ├── logger/
│   └── crypto/
├── api/
│   └── openapi.yaml
├── config/
│   └── config.yaml
├── scripts/
│   └── deploy.sh
├── go.mod
├── go.sum
└── README.md

Многие команды хранят небольшой общий пакет логирования в internal/ (например, internal/logx), чтобы бинарные файлы в cmd/ настраивали один логгер при запуске; pkg/logger/ в приведенной выше схеме уместна только тогда, когда этот код предназначен для импорта другими модулями. Для производственной настройки log/slog (JSON-строки, маскирование, поля контекста и трассировки, а также сигналы, хорошо работающие со стеками мониторинга), см. Структурированное логирование в Go с slog для наблюдаемости и оповещений.

Директория cmd/

Директория cmd/ содержит точки входа вашего приложения. Каждая поддиректория представляет отдельный исполняемый бинарный файл. Например, cmd/api/main.go собирает ваш API-сервер, а cmd/worker/main.go может собирать процессор фоновых задач.

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

// cmd/api/main.go
package main

import (
    "log"
    "myproject/internal/server"
    "myproject/internal/config"
)

func main() {
    cfg, err := config.Load()
    if err != nil {
        log.Fatal(err)
    }
    
    srv := server.New(cfg)
    if err := srv.Start(); err != nil {
        log.Fatal(err)
    }
}

Директория internal/

Здесь находится частый код вашего приложения. Компилятор Go предотвращает импорт пакетов из internal/ любыми внешними проектами, что делает их идеальными для:

  • Бизнес-логики и доменных моделей
  • Прикладных сервисов
  • Внутренних API и интерфейсов
  • Репозиториев базы данных (для выбора правильного ORM см. наше сравнение ORM для PostgreSQL в Go)
  • Логики аутентификации и авторизации

Организуйте internal/ по функциям или доменам, а не по техническим слоям. Вместо internal/handlers/, internal/services/, internal/repositories/, предпочитайте internal/user/, internal/order/, internal/payment/, где каждый пакет содержит свои обработчики, сервисы и репозитории.

Следует ли начинать со сложной структуры директорий?

Однозначно нет. Если вы создаете небольшой инструмент или прототип, начните с:

myproject/
├── main.go
├── go.mod
└── go.sum

По мере роста проекта и выявления логических группировок вводите директории. Вы можете добавить пакет db/, когда логика работы с базой данных станет существенной, или пакет api/, когда обработчиков HTTP станет слишком много. Позвольте структуре возникать естественно, а не навязывайте ее заранее.

Плоские против вложенных структур: поиск баланса

Одной из самых распространенных ошибок в структуре проектов на Go является чрезмерная вложенность. Go предпочитает мелкие иерархии — обычно на один или два уровня глубины. Глубокая вложенность увеличивает когнитивную нагрузку и делает импорты громозкими.

Каковы самые распространенные ошибки?

Избыточная вложенность директорий: Избегайте структур вида internal/services/user/handlers/http/v1/. Это создает ненужную сложность навигации. Вместо этого используйте internal/user/handler.go.

Общие имена пакетов: Имена вроде utils, helpers, common или base являются запахами кода (code smells). Они не передают конкретную функциональность и часто становятся свалкой для несвязанного кода. Используйте описательные имена: validator, auth, storage, cache.

Циклические зависимости: Когда пакет A импортирует пакет B, а B импортирует A, у вас возникает циклическая зависимость — ошибка компиляции в Go. Это обычно сигнализирует о плохом разделении обязанностей. Введите интерфейсы или вынесите общие типы в отдельный пакет.

Смешивание обязанностей: Держите обработчики HTTP сфокусированными на HTTP, репозитории баз данных — на доступе к данным, а бизнес-логику — в пакетах сервисов. Размещение бизнес-правил в обработчиках затрудняет тестирование и связывает вашу доменную логику с HTTP.

ДDD (Domain-Driven Design) и Гексагональная архитектура

Для крупных приложений, особенно микросервисов, Domain-Driven Design (DDD) с Гексагональной архитектурой обеспечивает четкое разделение обязанностей.

Как структурировать микросервис, следуя принципам DDD?

Гексагональная архитектура организует код в концентрические слои, где зависимости направлены внутрь:

internal/
├── domain/
│   └── user/
│       ├── entity.go        # Доменные модели и значения-объекты
│       ├── repository.go    # Интерфейс репозитория (порт)
│       └── service.go       # Доменные сервисы
├── application/
│   └── user/
│       ├── create_user.go   # Случай использования: создание пользователя
│       ├── get_user.go      # Случай использования: получение пользователя
│       └── service.go       # Оркестрация прикладных сервисов
├── adapter/
│   ├── http/
│   │   └── user_handler.go  # HTTP-адаптер (REST-концы)
│   ├── postgres/
│   │   └── user_repo.go     # Адаптер базы данных (реализует порт репозитория)
│   └── redis/
│       └── cache.go         # Адаптер кэша
└── api/
    └── http/
        └── router.go        # Конфигурация маршрутов и middleware

Доменный слой (domain/): Основная бизнес-логика, сущности, значения-объекты и интерфейсы доменных сервисов. Этот слой не имеет зависимостей от внешних систем — никаких импортов HTTP или баз данных. Он определяет интерфейсы репозиториев (порты), которые реализуют адаптеры.

Прикладной слой (application/): Случаи использования, которые оркестрируют доменные объекты. Каждый случай использования (например, “создать пользователя”, “обработать платеж”) — это отдельный файл или пакет. Этот слой координирует доменные объекты, но не содержит бизнес-правил сам по себе.

Слой адаптеров (adapter/): Реализует интерфейсы, определенные внутренними слоями. HTTP-обработчики преобразуют запросы в доменные объекты, репозитории баз данных реализуют персистентность, очереди сообщений обрабатывают асинхронную коммуникацию. Этот слой содержит весь код, специфичный для фреймворков и инфраструктуры.

Слой API (api/): Маршруты, middleware, DTO (объекты передачи данных), версионирование API и спецификации OpenAPI.

Эта структура гарантирует, что ваша основная бизнес-логика остается тестируемой и независимой от фреймворков, баз данных или внешних сервисов. Вы можете заменить PostgreSQL на MongoDB или REST на gRPC, не затрагивая доменный код. Если вы внедряете CQRS в эту структуру, Реализация CQRS в Go показывает, как слой application/ естественным образом отображается на отдельные пакеты обработчиков команд и запросов, сохраняя сторону команд строгой, а сторону запросов ориентированной на DTO.

Практические паттерны для различных типов проектов

Небольшой CLI-инструмент

Для приложений командной строки вам понадобится структура, поддерживающая несколько команд и подкоманд. Рассмотрите использование фреймворков, таких как Cobra для структуры команд и Viper для управления конфигурацией. Наше руководство по созданию CLI-приложений в Go с Cobra & Viper подробно охватывает этот паттерн.

mytool/
├── main.go
├── command/
│   ├── root.go
│   └── version.go
├── go.mod
└── README.md

REST API сервис

При создании REST API на Go эта структура четко разделяет обязанности: обработчики занимаются HTTP, сервисы содержат бизнес-логику, а репозитории управляют доступом к данным. Для комплексного руководства, охватывающего подходы стандартной библиотеки, фреймворки, аутентификацию, паттерны тестирования и лучшие практики, готовые к продакшену, см. наше полное руководство по созданию REST API в Go. Для структурированного JSON-логирования, корреляции запросов и трассировок, а также форматов логов, поддерживающих оповещения, см. Структурированное логирование в Go с slog для наблюдаемости и оповещений.

myapi/
├── cmd/
│   └── api/
│       └── main.go
├── internal/
│   ├── config/
│   ├── middleware/
│   ├── user/
│   │   ├── handler.go
│   │   ├── service.go
│   │   └── repository.go
│   └── product/
│       ├── handler.go
│       ├── service.go
│       └── repository.go
├── pkg/
│   └── httputil/
├── go.mod
└── README.md

Monorepo с несколькими сервисами

myproject/
├── cmd/
│   ├── api/
│   ├── worker/
│   └── scheduler/
├── internal/
│   ├── shared/        # Общие внутренние пакеты
│   ├── api/          # Код, специфичный для API
│   ├── worker/       # Код, специфичный для воркера
│   └── scheduler/    # Код, специфичный для планировщика
├── pkg/              # Общие библиотеки
├── go.work           # Файл рабочего пространства Go
└── README.md

Тестирование и документация

Размещайте файлы тестов рядом с тестируемым кодом, используя суффикс _test.go:

internal/
└── user/
    ├── service.go
    ├── service_test.go
    ├── repository.go
    └── repository_test.go

Этот стандарт держит тесты близко к реализации, облегчая их поиск и поддержку. Для интеграционных тестов, затрагивающих несколько пакетов, создайте отдельную директорию test/ в корне проекта. Для комплексного руководства по написанию эффективных модульных тестов, включая табличные тесты, моки, анализ покрытия и лучшие практики, см. наше руководство по структуре и лучшим практикам модульного тестирования в Go.

Документация должна находиться в:

  • README.md: Обзор проекта, инструкции по настройке, базовое использование
  • docs/: Подробная документация, архитектурные решения, ссылки на API
  • api/: Спецификации OpenAPI/Swagger, определения protobuf

Для REST API генерация и предоставление документации OpenAPI с Swagger необходимы для обнаружения API и опыта разработчика. Наше руководство по добавлению Swagger в ваш Go API охватывает интеграцию с популярными фреймворками и лучшие практики.

Управление зависимостями с помощью Go Modules

Каждый проект на Go должен использовать Go Modules для управления зависимостями. Для комплексного справочника по командам Go и управлению модулями проверьте наш Шпаргалку по Go. Инициализируйте с помощью:

go mod init github.com/yourusername/myproject

Это создает go.mod (зависимости и версии) и go.sum (контрольные суммы для проверки). Храните эти файлы в системе контроля версий для воспроизводимых сборок.

Регулярно обновляйте зависимости:

go get -u ./...          # Обновить все зависимости
go mod tidy              # Удалить неиспользуемые зависимости
go mod verify            # Проверить контрольные суммы

Ключевые выводы

  1. Начинайте с простого, развивайтесь естественно: Не усложняйте начальную структуру. Добавляйте директории и пакеты, когда этого требует сложность.

  2. Предпочитайте плоские иерархии: Ограничивайте вложенность одним или двумя уровнями. Плоская структура пакетов Go улучшает читаемость.

  3. Используйте описательные имена пакетов: Избегайте общих имен, таких как utils. Называйте пакеты в соответствии с их функцией: auth, storage, validator.

  4. Четко разделяйте обязанности: Держите обработчики сфокусированными на HTTP, репозитории — на доступе к данным, а бизнес-логику — в пакетах сервисов.

  5. Используйте internal/ для приватности: Используйте его для кода, который не должен импортироваться внешними системами. Бóльшая часть кода приложения должна находиться здесь.

  6. Применяйте архитектурные паттерны при необходимости: Для сложных систем Гексагональная архитектура и DDD обеспечивают четкие границы и тестируемость.

  7. Позвольте Go направлять вас: Следуйте идиомам Go, а не импортируйте паттерны из других языков. У Go своя философия простоты и организации.

Полезные ссылки

Другие связанные статьи

Подписаться

Получайте новые материалы про системы, инфраструктуру и AI engineering.