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
Представьте жизненный сценарий:
- Команде сервиса
C(Профили пользователей) потребовалось добавить новое поле в модельUserи изменить сигнатуру метода в общем логгере. - Они коммитят изменения в
company-coreи публикуют релизcompany-core v2.0.0с ломающими обратную совместимость изменениями (Breaking Changes). - На следующий день в сервисе
A(Платежи) обнаруживается критическая уязвимость безопасности. Чтобы применить патч из библиотекиcompany-core, разработчики сервисаAвынуждены подтянуть версиюv2.0.0. - Но проект сервиса
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 или инициализация глобальных конфигураций.
// Пакет внутри 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. Но эта библиотека обязана быть предельно тонкой и не скрывать нативные интерфейсы за самодельными сложными фасадами:
// ✅ ПРАВИЛЬНО: Тонкая обертка возвращает стандартный интерфейс OTel
package telemetry
import "go.opentelemetry.io/otel/trace"
func InitTracer(serviceName string) (trace.TracerProvider, error) {
// Единая настройка OTLP экспортера по стандартам компании
return provider, nil
}
// ❌ АНТИПАТТЕРН: Собственный велосипед, блокирующий экосистему
package companycore
// Скрыли стандартный OTel за самодельным типом, сломав совместимость
func StartSpan(name string) MyCustomSpan { ... }
Копипаст (Deduplication via Copy-Paste)
Когда вы пишете новый микросервис на Go и вам требуется вспомогательная функция для извлечения Bearer-токена из заголовка Authorization или простая middleware для логирования задержки HTTP-хендлера:
- Не создавайте библиотеку немедленно.
- Скопируйте 25 строк проверенного кода из соседнего репозитория. В Go эти функции лаконичны, понятны и не требуют поддержки.
- Соблюдайте «Правило трех» (Rule of Three):
- Первый раз: пишите код по месту.
- Второй раз: скопируйте код без зазрения совести.
- Третий раз: только если абсолютно идентичный фрагмент кода потребовался в третий раз, и вы на 100% уверены, что исправление бага в этом алгоритме обязано происходить синхронно во всех местах — только тогда инвестируйте время в создание выделенного, тщательно протестированного утилитарного пакета.
Вопросы для собеседования
Резюме
- Меньше DRY, больше независимости: Копирование 30 строк простого кода лучше, чем создание тяжелой разделяемой библиотеки, связывающей релизные циклы команд.
- Изоляция доменных моделей: Никогда не выносите доменные сущности (
User,Order) в общие модули. Модель должна принадлежать своему Bounded Context. - MVS и чистые библиотеки: Внешние библиотеки обязаны иметь минимальное количество сторонних зависимостей (в идеале — 0), чтобы исключить конфликты версий в Go Modules.
- Кодогенерация API: Для взаимодействия между сервисами используйте генерацию клиентских SDK из формальных спецификаций (Protobuf / OpenAPI), а не рукописные общие пакеты.
- Правило трех: Прежде чем абстрагировать и выносить код в библиотеку, позвольте ему проявиться как минимум в трех независимых местах.
Мы разобрались с границами переиспользования кода и модульной изоляцией. Но как теперь физически организовать хранение сотен микросервисов в системе контроля версий? Создавать ли отдельный Git-репозиторий под каждый сервис (Polyrepo), или сложить всю кодовую базу компании в единый гигантский монорепозиторий (Monorepo), как это делают Google и Uber? В следующей лекции мы детально исследуем эту инженерную дилемму: 3. Monorepo vs polyrepo.