Монорепозиторий с Go: организация кода на практике
Монорепозиторий (monorepo) — это подход к управлению кодом, при котором все проекты, библиотеки и сервисы хранятся в одном репозитории. В сообществе Go этот подход набирает популярность благодаря своей простоте и эффективности. В этой статье мы разберем, как организовать Go-проект в монорепозитории на практике: от выбора структуры до настройки CI/CD.
Почему монорепозиторий для Go?
Go изначально проектировался как язык для больших кодовых баз. Инструменты вроде go mod и go work делают работу с монорепозиторием естественной. Основные преимущества:
- Упрощение зависимостей: все модули используют единую версию пакетов, нет конфликтов версий.
- Единый CI/CD: один файл конфигурации для всех сервисов, легче поддерживать.
- Рефакторинг: изменение API в библиотеке сразу отражается на всех потребителях в репозитории.
- Единые стандарты: линтеры, форматеры и код-ревью применяются ко всему коду.
Структура монорепозитория: проверенная схема
Рассмотрим структуру, которая отлично работает для Go-проектов среднего размера. Предположим, у нас есть два сервиса: auth-service и api-gateway, а также общие библиотеки.
my-project/
├── apps/ # Исполняемые приложения
│ ├── auth-service/
│ │ ├── cmd/
│ │ │ └── main.go
│ │ ├── internal/
│ │ │ ├── handler/
│ │ │ ├── service/
│ │ │ └── repository/
│ │ ├── go.mod
│ │ └── go.sum
│ └── api-gateway/
│ ├── cmd/
│ │ └── main.go
│ ├── internal/
│ │ ├── middleware/
│ │ └── routes/
│ ├── go.mod
│ └── go.sum
├── libs/ # Общие библиотеки
│ ├── config/
│ │ ├── config.go
│ │ ├── go.mod
│ │ └── go.sum
│ ├── logger/
│ │ ├── logger.go
│ │ ├── go.mod
│ │ └── go.sum
│ └── db/
│ ├── postgres.go
│ ├── go.mod
│ └── go.sum
├── scripts/ # Скрипты для CI/CD и утилит
│ ├── build.sh
│ └── test.sh
├── go.work # Workspace Go (опционально)
├── .golangci.yml # Конфигурация линтера
├── Makefile # Общие команды
└── README.md
Ключевые элементы структуры
apps/— все исполняемые приложения. Каждое приложение имеет свою точку входа (cmd/main.go) и собственныйgo.mod.libs/— библиотеки общего пользования. Каждая библиотека — отдельный модуль Go.scripts/— вспомогательные скрипты для сборки, тестирования и развертывания.go.work— файл для Go Workspace, упрощает локальную разработку.
Настройка Go Workspace для локальной разработки
Go 1.18+ поддерживает Workspace через файл go.work. Это позволяет локально ссылаться на библиотеки из libs/ без публикации в удаленный репозиторий.
Создайте go.work в корне проекта:
go 1.22
use (
./apps/auth-service
./apps/api-gateway
./libs/config
./libs/logger
./libs/db
)
Теперь при разработке вы можете импортировать библиотеки напрямую:
// apps/auth-service/cmd/main.go
package main
import (
"my-project/libs/config"
"my-project/libs/logger"
)
func main() {
cfg := config.Load()
log := logger.New(cfg.LogLevel)
log.Info("Auth service started")
}
Важное замечание: в go.mod каждого приложения зависимость от библиотеки должна указывать на локальный путь через replace:
// apps/auth-service/go.mod
module github.com/company/my-project/apps/auth-service
go 1.22
require (
github.com/company/my-project/libs/config v0.0.0
github.com/company/my-project/libs/logger v0.0.0
)
replace (
github.com/company/my-project/libs/config => ../../libs/config
github.com/company/my-project/libs/logger => ../../libs/logger
)
Правила для internal-пакетов
Go имеет встроенный механизм ограничения видимости — internal. Пакеты внутри директории internal могут быть импортированы только из родительской и дочерних директорий. Это отличный способ скрыть внутреннюю реализацию сервиса.
Пример для auth-service:
apps/auth-service/
├── cmd/
│ └── main.go
├── internal/
│ ├── handler/
│ │ └── auth_handler.go
│ ├── service/
│ │ └── auth_service.go
│ └── repository/
│ └── user_repo.go
├── go.mod
└── go.sum
Код из internal/handler может импортировать internal/service, но не может быть импортирован из api-gateway или других сервисов. Это защищает от случайных зависимостей и поощряет четкое разделение ответственности.
// internal/service/auth_service.go
package service
import "my-project/apps/auth-service/internal/repository"
type AuthService struct {
repo *repository.UserRepo
}
func NewAuthService(repo *repository.UserRepo) *AuthService {
return &AuthService{repo: repo}
}
CI/CD для монорепозитория
Одна из главных задач — настроить пайплайн так, чтобы при изменении в одном сервисе не собирались все остальные. Используйте подход "changed files" для определения целей сборки.
Пример для GitHub Actions:
# .github/workflows/ci.yml
name: CI
on:
push:
branches: [main]
pull_request:
jobs:
detect-changes:
runs-on