Монорепозиторий с Go: организация кода для масштабируемых проектов

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

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

Начать курс

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

Блоги

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