9.8 KiB
👋 Привет! Я Олег Козырев
Staff Golang Engineer · Ex Ozon, Avito, Tinkoff
Dependency Injection в Go: от антипаттернов к элегантным решениям
Репозиторий с примерами различных подходов к внедрению зависимостей (DI) в Go-приложениях.
Все примеры используют одну и ту же бизнес-логику из internal/ — меняется только способ сборки зависимостей.
Структура проекта
internal/
├── config/ — конфигурация приложения (DSN, Redis, HTTP-адрес)
├── database/ — абстракция БД (интерфейс DB + реализация)
├── cache/ — абстракция кэша (интерфейс Cache + Redis-заглушка)
├── events/ — шина событий (Publish/Subscribe)
├── repository/ — репозитории (user, session, notification)
├── service/ — сервисы (auth, user, notification)
├── api/ — HTTP-хендлер и роутинг
└── app/ — сборка приложения + кастомный DI-контейнер
cmd/
├── antipattern/ — ручная сборка (проблема)
├── antipattern-broken/ — ручная сборка (сломанная)
├── wire-di/ — Google Wire
├── wire-di-broken/ — Wire + циклические зависимости
├── fx-di/ — Uber Fx (свои интерфейсы у каждого потребителя)
├── fx-di-broken/ — Fx + циклические зависимости
├── fx-di-shared/ — Uber Fx (общие интерфейсы)
├── fx-di-modules/ — Uber Fx (модульная организация)
└── manual-di/ — кастомный lazy-контейнер (решение)
Ключевой паттерн: интерфейс определяет потребитель
Каждый сервис и репозиторий определяет свой собственный интерфейс для каждой зависимости — только с теми методами, которые ему нужны. Это Go-идиома и принцип разделения интерфейсов (ISP).
Например, UserRepo определяет интерфейс UserDB с нужными ему методами, а SessionRepo — свой SessionDB. Оба реализуются одним и тем же *database.db.
Примеры
1. cmd/antipattern/ — Ручная сборка зависимостей
Все зависимости создаются вручную в main() в строгом порядке. Работает, но:
- порядок инициализации жёстко зафиксирован
- при добавлении зависимости легко ошибиться
- код main() растёт линейно с количеством компонентов
go run ./cmd/antipattern/
2. cmd/antipattern-broken/ — Сломанная ручная сборка
Тот же код, но строки переставлены местами. Компилируется без ошибок, но падает в рантайме с nil pointer dereference — кэш и сервисы используются до инициализации.
Показывает, почему ручная сборка хрупкая: компилятор не ловит ошибки порядка инициализации.
go run ./cmd/antipattern-broken/
# curl http://localhost:8080/users/me → panic: nil pointer
# curl http://localhost:8080/health → 200 OK (зависимости не задействованы)
3. cmd/wire-di/ — Google Wire (кодогенерация)
Разработчик описывает провайдеры и привязки в wire.go, Wire генерирует корректный код инициализации в wire_gen.go.
Решает проблему порядка, но:
- 18 вызовов
wire.Bind()для маппинга типов → интерфейсы - eager-инициализация (всё создаётся сразу при старте)
- когда каждый потребитель определяет свой интерфейс, количество Bind растёт быстро
task generate-wire # генерация кода
go run ./cmd/wire-di/
4. cmd/wire-di-broken/ — Wire и циклические зависимости
Демонстрирует цикл: EventBus → NotificationService → UserService → AuthService → EventBus.
Wire обнаруживает цикл на этапе генерации и отказывается генерировать код.
task generate-wire-broken # ошибка: cycle for *EventBus
5. cmd/fx-di/ — Uber Fx (свои интерфейсы у каждого потребителя)
Рантайм DI-контейнер на рефлексии. Работает, но когда каждый потребитель определяет свой интерфейс — требует много бойлерплейта:
fx.Out/fx.Inструктуры для каждого провайдера- именованные зависимости через struct-теги
- ~360 строк кода
Показывает, что Fx заточен под «один тип — один интерфейс» (как в Java/Spring), и при Go-подходе к интерфейсам становится громоздким.
go run ./cmd/fx-di/
6. cmd/fx-di-broken/ — Fx и циклические зависимости
Тот же цикл, что и в wire-di-broken, но обнаруживается в рантайме при старте приложения.
go run ./cmd/fx-di-broken/ # ошибка: cannot depend on the provided type
7. cmd/fx-di-shared/ — Uber Fx (общие интерфейсы)
Fx с «Java-style» архитектурой: один общий интерфейс на тип. Код значительно проще (~56 строк), lifecycle-хуки для graceful старта и остановки.
Показывает, что Fx отлично работает, когда интерфейсы общие, а не определяются каждым потребителем отдельно.
go run ./cmd/fx-di-shared/
8. cmd/fx-di-modules/ — Uber Fx (модульная организация)
Продвинутый пример с fx.Module: каждый домен (auth, user, notification) — отдельный модуль в отдельном файле.
Подход для больших команд: каждая команда редактирует только свой модуль, конфликтов в main.go нет.
go run ./cmd/fx-di-modules/
9. cmd/manual-di/ — Кастомный lazy-контейнер (решение)
Финальное решение: кастомный DI-контейнер из internal/app/di.go с ленивой инициализацией.
- ~17 строк в main, ~50 строк в контейнере
- зависимости создаются при первом обращении (lazy)
- рекурсивное разрешение зависимостей — порядок не важен
- никакой магии, рефлексии и кодогенерации
- ошибки ловятся на этапе компиляции
go run ./cmd/manual-di/
Сравнение подходов
| Пример | Подход | Обнаружение ошибок | Бойлерплейт | Циклы |
|---|---|---|---|---|
antipattern |
Ручная сборка | Рантайм (nil pointer) | Нет | Не обнаруживает |
wire-di |
Кодогенерация | Генерация кода | 18 Bind() | Ловит при генерации |
fx-di |
Рефлексия (runtime) | Рантайм (старт) | fx.In/Out структуры | Ловит при старте |
fx-di-shared |
Рефлексия (runtime) | Рантайм (старт) | Минимальный | Ловит при старте |
fx-di-modules |
Рефлексия (runtime) | Рантайм (старт) | Минимальный | Ловит при старте |
manual-di |
Lazy-контейнер | Компиляция | Нет | Решает через lazy |
Требования
- Go 1.26+
- Task — таск-раннер (замена Make)
Установка Task
# macOS
brew install go-task
# Linux (snap)
sudo snap install task --classic
# Или через go install
go install github.com/go-task/task/v3/cmd/task@latest
Запуск
# Установка зависимостей
go mod download
# Установка Wire (для примеров с кодогенерацией)
task install-wire
# Запуск любого примера
go run ./cmd/<имя-примера>/
Доступные task-команды
task install-wire # установить Wire локально в bin/
task generate-wire # сгенерировать wire_gen.go для cmd/wire-di/
task generate-wire-broken # попытка генерации с циклом (упадёт с ошибкой)