2. Shared libraries vs copy paste

Цена независимости: Меньше DRY, больше автономности

«Небольшое копирование лучше, чем небольшая зависимость».
— Роб Пайк, Go Proverbs

Когда разработчики с солидным багажом классической enterprise-разработки (Java, C#, Ruby) приходят в архитектуру распределенных систем на Go, у них срабатывает глубоко укоренившийся рефлекс DRY (Don't Repeat Yourself — «Не повторяйся»).

Стоит инженеру заметить в трех разных микросервисах похожие структуры данных User или идентичные функции парсинга токена в заголовке Authorization, как в его голове загорается сигнал тревоги: «Дублирование кода! Это неаккуратно! Нужно срочно навести порядок!». Немедленно создается общий git-репозиторий с гордым именем company-core (или shared-lib, common, platform-go). В него бережно складываются все общие структуры, обертки над базами данных, утилиты логирования и middleware. А затем эта общая библиотека директивой require в файле go.mod жестко привязывается ко всем микросервисам компании.

В эту самую секунду распределенная микросервисная архитектура тихо умирает. На свет появляется худший кошмар системного инженера — Распределенный монолит (Distributed Monolith).

В этой лекции мы разберем, почему копирование небольших фрагментов кода в мире Go — это не грех, а осознанная архитектурная необходимость, как общие библиотеки взрывают граф зависимостей через механизм MVS, чем опасны функции init() и как концепция Bounded Context спасает автономию инженерных команд.


Иллюзия переиспользования и Распределенный монолит

Главная и единственная причина, оправдывающая переход от простого монолита к микросервисам — это независимость развертывания (Independent Deployability) и организационная автономия команд. Команда сервиса A обязана иметь возможность выкатить релиз в боевой кластер в любой момент времени, не согласуя свои действия с командой сервиса B и не дожидаясь релиза сервиса C.

Что происходит на практике, когда в инфраструктуру внедряется тяжелая библиотека company-core?

flowchart TD
    Core["Библиотека company-core v1.5.0\n• Доменные структуры: User, Order\n• Настройки log/slog\n• Middleware авторизации"]

    ServiceA["Микросервис A (Платежи)"] -->|"Импорт v1.5.0"| Core
    ServiceB["Микросервис B (Уведомления)"] -->|"Импорт v1.5.0"| Core
    ServiceC["Микросервис C (Профили)"] -->|"Импорт v1.5.0"| Core

    style Core fill:#1e293b,stroke:#f59e0b,stroke-width:2px,color:#f8fafc
  

Представьте жизненный сценарий:

  1. Команде сервиса C (Профили пользователей) потребовалось добавить новое поле в модель User и изменить сигнатуру метода в общем логгере.
  2. Они коммитят изменения в company-core и публикуют релиз company-core v2.0.0 с ломающими обратную совместимость изменениями (Breaking Changes).
  3. На следующий день в сервисе A (Платежи) обнаруживается критическая уязвимость безопасности. Чтобы применить патч из библиотеки company-core, разработчики сервиса A вынуждены подтянуть версию v2.0.0.
  4. Но проект сервиса A не компилируется! Компилятор Go требует переписать половину кода сервиса под новые сигнатуры методов логгера и новую модель User, которые сервису платежей даром не нужны.
flowchart TD
    subgraph Hell["Капкан распределенного монолита"]
        CoreNew["company-core v2.0.0\n(Breaking Changes в User и логгере)"]
        ServiceC["Микросервис C (Профили)\n✅ Обновлен под v2.0.0"] --> CoreNew
        ServiceA["Микросервис A (Платежи)\n💥 Ошибка компиляции! Срочный хотфикс заблокирован!"] -.->|"Зависимость застряла"| CoreNew
        ServiceB["Микросервис B (Уведомления)\n⚠️ Боится обновляться"] -.-> CoreNew
    end

    style ServiceA fill:#3b1d22,stroke:#f43f5e,stroke-width:2px,color:#f8fafc
  

В этот момент автономия команд испаряется. Релизы становятся синхронными: чтобы обновить один компонент, инженеры вынуждены собирать совещания, тестировать весь зоопарк сервисов и координировать выкатки.

Вы получили все недостатки монолита (жесткая связность, каскадные поломки при сборке) и одновременно все недостатки распределенных систем (сетевой оверхед, сериализация JSON/Protobuf, частичные отказы и сложные сетевые сбои).


Mechanical Sympathy: Под капотом общих библиотек

Помимо организационного паралича, раздутые общие библиотеки создают серьезные технические аномалии на уровне компилятора и рантайма Go.

1. Угроза функций init()

В языке Go существует специальная функция init(), которая не принимает аргументов, не возвращает значений и исполняется рантаймом автоматически в момент загрузки пакета строго до старта функции main.

В крупных «универсальных» библиотеках company-core разработчики часто поддаются соблазну использовать init() для скрытой автоматической регистрации: подключение SQL-драйверов, регистрация метрик Prometheus или инициализация глобальных конфигураций.

Go
// Пакет внутри company-core/database/postgres:
package postgres

import (
    "database/sql"
    _ "github.com/lib/pq" // Функция init() регистрирует драйвер 'postgres'
)

func init() {
    // Скрытая магия: регистрация глобальных счетчиков
}

2. MVS (Minimal Version Selection)

Для разрешения графа зависимостей менеджер пакетов Go Modules использует детерминированный алгоритм Расса Кокса — MVS (Minimal Version Selection — Выбор минимальной версии).

В отличие от пакетных менеджеров других языков (npm, pip, cargo), которые стремятся установить самую свежую совместимую версию (SemVer Max), Go выбирает минимальную версию из всех, явно запрошенных в дереве зависимостей.

flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    MyApp["Ваш Go микросервис"]:::process -->|"Требует"| LibCore["company-core v1.2.0"]
    MyApp -->|"Явно требует"| GrpcDirect["grpc-go v1.55"]
    LibCore -->|"Транзитивно требует"| GrpcTransitive["grpc-go v1.50"]

    GrpcDirect -.->|"MVS выбирает v1.55"| Resolved["Финальная сборка: grpc-go v1.55"]
  

Но если библиотека company-core тянет за собой десятки сторонних зависимостей (AWS SDK, Kafka Sarama, Redis client, gRPC), возникает классический Dependency Hell (Ад зависимостей):

  • Любое неаккуратное обновление транзитивной библиотеки внутри company-core способно сломать типы данных в вашем микросервисе.
  • Попытка изолированно обновить безопасность уязвимого пакета превращается в войну с файлом go.mod.

Bounded Context: Почему копипаст структур — это норма

Главный психологический барьер разработчиков формулируется вопросом: «Неужели мне нужно объявлять структуру User или Product в каждом сервисе заново? Это же чистый копипаст!».

Ответ современной программной инженерии: Да, именно так!

flowchart TD
    classDef process  fill:#1e293b,stroke:#38bdf8,stroke-width:1px,color:#f8fafc
    classDef success  fill:#064e3b,stroke:#34d399,stroke-width:1px,color:#f8fafc
    classDef error    fill:#7f1d1d,stroke:#f87171,stroke-width:1px,color:#f8fafc
    classDef warning  fill:#78350f,stroke:#fbbf24,stroke-width:1px,color:#f8fafc
    classDef entry    fill:#1e3a5f,stroke:#38bdf8,stroke-width:2px,color:#f8fafc
    classDef aux      fill:#334155,stroke:#64748b,stroke-width:1px,color:#f8fafc
    classDef system   fill:#312e81,stroke:#a78bfa,stroke-width:1px,color:#f8fafc
    classDef data     fill:#132d29,stroke:#2dd4bf,stroke-width:1px,color:#f8fafc
    classDef network  fill:#881337,stroke:#fb7185,stroke-width:1px,color:#f8fafc
    classDef accent   fill:#1e1b4b,stroke:#818cf8,stroke-width:1px,color:#f8fafc
    subgraph BadWay["Антипаттерн: Uber-Product в shared-lib"]
        Uber["struct Product (50 полей!)\nГабариты + Фотографии + НДС + SEO + Цена"]
        S1["Склад"] --> Uber
        S2["Каталог"] --> Uber
        S3["Биллинг"] --> Uber
    end

    subgraph GoodWay["Идиоматичный подход: Bounded Contexts"]
        P1["Склад: struct StockItem\n(Weight, Dimensions, ShelfID)"]
        P2["Каталог: struct CatalogProduct\n(Title, Images, Description)"]
        P3["Биллинг: struct InvoiceItem\n(ID, SKU, PriceCents, TaxRate)"]
    end
  

В соответствии с правилами чистой архитектуры (см. 1. Структура микросервисного проекта на Go), каждый микросервис объявляет в своем пакете internal/domain собственную структуру. Да, в них совпадут базовые поля ID и Name. Но это совпадение случайно — у этих полей разные владельцы, разные жизненные циклы и разные темпы изменений (см. подробнее: 2. Границы сервисов).


Что МОЖНО и НУЖНО выносить в общие библиотеки?

Означает ли это, что общие библиотеки запрещены полностью? Разумеется, нет. Выносить в модули следует код, обладающий высокой технической связностью и абсолютно нулевой бизнес-логикой.

1. Сгенерированные SDK и контракты API

Когда сервис A предоставляет gRPC или REST API, он обязан предоставить потребителям готовый клиент.
Идеальный подход — автоматическая кодогенерация из формальных контрактов (Protocol Buffers или OpenAPI спецификаций). Сгенерированный код складывается в отдельный легковесный репозиторий (например, github.com/company/payment-api-go).
Потребители импортируют только тонкий SDK клиента. В нем нет бизнес-логики сервиса, только сетевой транспорт и структуры запросов/ответов.

2. Чистые функции и криптография (Pure Functions)

Функции, которые детерминированно преобразуют входные аргументы в результат и не имеют побочных эффектов:

  • Кастомный алгоритм вычисления контрольной суммы или хеширования паролей;
  • Специфичная для компании математика округления финансовых сумм;
  • Валидаторы стандартизированных номеров (номера паспортов, налоговые идентификаторы).

3. Инфраструктурные обертки (с осторожностью)

Допускается создание корпоративного пакета platform-go, стандартизирующего интеграцию с OpenTelemetry или log/slog. Но эта библиотека обязана быть предельно тонкой и не скрывать нативные интерфейсы за самодельными сложными фасадами:

Go
// ✅ ПРАВИЛЬНО: Тонкая обертка возвращает стандартный интерфейс OTel
package telemetry

import "go.opentelemetry.io/otel/trace"

func InitTracer(serviceName string) (trace.TracerProvider, error) {
    // Единая настройка OTLP экспортера по стандартам компании
    return provider, nil
}
Go
// ❌ АНТИПАТТЕРН: Собственный велосипед, блокирующий экосистему
package companycore

// Скрыли стандартный OTel за самодельным типом, сломав совместимость
func StartSpan(name string) MyCustomSpan { ... }

Копипаст (Deduplication via Copy-Paste)

Когда вы пишете новый микросервис на Go и вам требуется вспомогательная функция для извлечения Bearer-токена из заголовка Authorization или простая middleware для логирования задержки HTTP-хендлера:

  1. Не создавайте библиотеку немедленно.
  2. Скопируйте 25 строк проверенного кода из соседнего репозитория. В Go эти функции лаконичны, понятны и не требуют поддержки.
  3. Соблюдайте «Правило трех» (Rule of Three):
    • Первый раз: пишите код по месту.
    • Второй раз: скопируйте код без зазрения совести.
    • Третий раз: только если абсолютно идентичный фрагмент кода потребовался в третий раз, и вы на 100% уверены, что исправление бага в этом алгоритме обязано происходить синхронно во всех местах — только тогда инвестируйте время в создание выделенного, тщательно протестированного утилитарного пакета.

Вопросы для собеседования


Резюме

  1. Меньше DRY, больше независимости: Копирование 30 строк простого кода лучше, чем создание тяжелой разделяемой библиотеки, связывающей релизные циклы команд.
  2. Изоляция доменных моделей: Никогда не выносите доменные сущности (User, Order) в общие модули. Модель должна принадлежать своему Bounded Context.
  3. MVS и чистые библиотеки: Внешние библиотеки обязаны иметь минимальное количество сторонних зависимостей (в идеале — 0), чтобы исключить конфликты версий в Go Modules.
  4. Кодогенерация API: Для взаимодействия между сервисами используйте генерацию клиентских SDK из формальных спецификаций (Protobuf / OpenAPI), а не рукописные общие пакеты.
  5. Правило трех: Прежде чем абстрагировать и выносить код в библиотеку, позвольте ему проявиться как минимум в трех независимых местах.

Мы разобрались с границами переиспользования кода и модульной изоляцией. Но как теперь физически организовать хранение сотен микросервисов в системе контроля версий? Создавать ли отдельный Git-репозиторий под каждый сервис (Polyrepo), или сложить всю кодовую базу компании в единый гигантский монорепозиторий (Monorepo), как это делают Google и Uber? В следующей лекции мы детально исследуем эту инженерную дилемму: 3. Monorepo vs polyrepo.