Монорепозиторий с Go: организация кода на практике

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Монорепозиторий с 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                

Блоги

Рекомендаций по книгам