Монорепозиторий с Go: организация кода для масштабируемых проектов
В современной разработке программного обеспечения все чаще возникает вопрос: как организовать код, чтобы он оставался управляемым, переиспользуемым и масштабируемым? Одним из популярных решений является использование монорепозитория (monorepo) — единого репозитория, содержащего несколько проектов или сервисов. В этой статье мы разберем, как эффективно организовать монорепозиторий на языке Go (Golang), рассмотрим лучшие практики, структуру каталогов, работу с модулями и настройку CI/CD.
Что такое монорепозиторий и зачем он нужен в Go?
Монорепозиторий — это подход, при котором весь код компании или крупного проекта хранится в одном репозитории. Это может включать:
- Несколько микросервисов
- Библиотеки и утилиты
- Конфигурации и скрипты
- Документацию
Преимущества монорепозитория для Go:
- Единая версия зависимостей — исключает конфликты версий между сервисами.
- Упрощение рефакторинга — изменения в общей библиотеке сразу видны всем потребителям.
- Легкое переиспользование кода — не нужно публиковать пакеты в реестр.
- Единая история коммитов — упрощает отладку и аудит.
Однако монорепозиторий требует дисциплины: без четкой структуры он быстро превращается в "свалку". Go с его строгой системой модулей (Go Modules) предоставляет отличные инструменты для организации такого репозитория.
Структура каталогов в монорепозитории Go
Правильная структура — основа успеха. Рассмотрим рекомендуемую организацию для монорепозитория на Go:
my-monorepo/
├── apps/ # Приложения (сервисы, CLI-утилиты)
│ ├── api-gateway/
│ │ ├── cmd/
│ │ │ └── main.go
│ │ ├── internal/
│ │ ├── go.mod
│ │ └── go.sum
│ ├── auth-service/
│ │ └── ...
│ └── user-service/
│ └── ...
├── libs/ # Общие библиотеки
│ ├── common/
│ │ ├── logger.go
│ │ ├── config.go
│ │ ├── go.mod
│ │ └── go.sum
│ └── models/
│ ├── user.go
│ ├── go.mod
│ └── go.sum
├── scripts/ # Скрипты сборки, деплоя
│ ├── build.sh
│ └── deploy.sh
├── docs/ # Документация
├── .github/ # CI/CD конфигурации
│ └── workflows/
│ └── ci.yml
├── go.work # Workspace файл (Go 1.18+)
├── Makefile # Общие команды
├── README.md
└── .gitignore
Ключевые элементы структуры:
- apps/ — содержит исполняемые приложения (сервисы, утилиты). Каждое приложение — отдельный модуль Go с собственным
go.mod. - libs/ — общие библиотеки, которые переиспользуются между приложениями.
- go.work — файл workspace, позволяющий работать со всеми модулями локально без публикации изменений.
Работа с Go Modules в монорепозитории
Начиная с Go 1.18, появилась поддержка workspaces (go.work). Это решает проблему локальной разработки, когда модули зависят друг от друга. Рассмотрим пример настройки.
Создадим файл go.work в корне монорепозитория:
go 1.21
use (
./apps/api-gateway
./apps/auth-service
./libs/common
./libs/models
)
Теперь при локальной разработке Go будет использовать локальные версии модулей, а не опубликованные. Это критически важно для быстрой итерации.
Пример зависимости в apps/auth-service/go.mod:
module github.com/company/auth-service
go 1.21
require (
github.com/company/libs/common v0.0.0
github.com/company/libs/models v0.0.0
)
// Директива replace для локальной разработки (если не используется workspace)
replace (
github.com/company/libs/common => ../../libs/common
github.com/company/libs/models => ../../libs/models
)
При использовании workspace директива replace не нужна — Go сам разрешает зависимости через go.work.
Пример кода: библиотека логирования
Рассмотрим простую библиотеку для логирования в libs/common/logger.go:
package common
import (
"log"
"os"
)
type Logger struct {
logger *log.Logger
}
func NewLogger() *Logger {
return &Logger{
logger: log.New(os.Stdout, "INFO: ", log.Ldate|log.Ltime|log.Lshortfile),
}
}
func (l *Logger) Info(msg string, keysAndValues ...interface{}) {
l.logger.Println(append([]interface{}{msg}, keysAndValues...)...)
}
func (l *Logger) Error(msg string, err error, keysAndValues ...interface{}) {
l.logger.Println(append([]interface{}{"ERROR:", msg, "error:", err}, keysAndValues...)...)
}
И использование этой библиотеки в сервисе apps/auth-service:
package main
import (
"github.com/company/libs/common"
)
func main() {
logger := common.NewLogger()
logger.Info("Auth service started", "port", 8080)
// ... логика сервиса
if err := someFunction(); err != nil {
logger.Error("Failed to process request", err, "user_id", 12345)
}
}
CI/CD для монорепозитория Go
Одна из главных проблем монорепозитория — как запускать только нужные тесты и сборки при изменении кода. Решение — умные CI/CD пайплайны, которые определяют, какие проекты были затронуты.
Пример GitHub Actions workflow для Go монорепозитория:
name: CI
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
jobs:
detect-changes:
runs-on: ubuntu-latest