Files

199 lines
9.8 KiB
Markdown
Raw Permalink Normal View History

2026-04-13 08:14:09 +03:00
## 👋 Привет! Я Олег Козырев
Staff Golang Engineer · Ex Ozon, Avito, Tinkoff
📺 [YouTube](https://www.youtube.com/@olezhek28go) · 💬 [Telegram](https://t.me/olezhek28go)
---
# 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() растёт линейно с количеством компонентов
```bash
go run ./cmd/antipattern/
```
### 2. `cmd/antipattern-broken/` — Сломанная ручная сборка
Тот же код, но строки переставлены местами. **Компилируется без ошибок**, но падает в рантайме с nil pointer dereference — кэш и сервисы используются до инициализации.
Показывает, почему ручная сборка хрупкая: компилятор не ловит ошибки порядка инициализации.
```bash
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 растёт быстро
```bash
task generate-wire # генерация кода
go run ./cmd/wire-di/
```
### 4. `cmd/wire-di-broken/` — Wire и циклические зависимости
Демонстрирует цикл: `EventBus → NotificationService → UserService → AuthService → EventBus`.
Wire обнаруживает цикл **на этапе генерации** и отказывается генерировать код.
```bash
task generate-wire-broken # ошибка: cycle for *EventBus
```
### 5. `cmd/fx-di/` — Uber Fx (свои интерфейсы у каждого потребителя)
Рантайм DI-контейнер на рефлексии. Работает, но когда каждый потребитель определяет свой интерфейс — требует много бойлерплейта:
- `fx.Out`/`fx.In` структуры для каждого провайдера
- именованные зависимости через struct-теги
- ~360 строк кода
Показывает, что Fx заточен под «один тип — один интерфейс» (как в Java/Spring), и при Go-подходе к интерфейсам становится громоздким.
```bash
go run ./cmd/fx-di/
```
### 6. `cmd/fx-di-broken/` — Fx и циклические зависимости
Тот же цикл, что и в `wire-di-broken`, но обнаруживается **в рантайме** при старте приложения.
```bash
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 отлично работает, когда интерфейсы общие, а не определяются каждым потребителем отдельно.
```bash
go run ./cmd/fx-di-shared/
```
### 8. `cmd/fx-di-modules/` — Uber Fx (модульная организация)
Продвинутый пример с `fx.Module`: каждый домен (auth, user, notification) — отдельный модуль в отдельном файле.
Подход для больших команд: каждая команда редактирует только свой модуль, конфликтов в `main.go` нет.
```bash
go run ./cmd/fx-di-modules/
```
### 9. `cmd/manual-di/` — Кастомный lazy-контейнер (решение)
Финальное решение: кастомный DI-контейнер из `internal/app/di.go` с ленивой инициализацией.
- ~17 строк в main, ~50 строк в контейнере
- зависимости создаются при первом обращении (lazy)
- рекурсивное разрешение зависимостей — порядок не важен
- никакой магии, рефлексии и кодогенерации
- ошибки ловятся на этапе компиляции
```bash
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](https://taskfile.dev/) — таск-раннер (замена Make)
### Установка Task
```bash
# 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
```
## Запуск
```bash
# Установка зависимостей
go mod download
# Установка Wire (для примеров с кодогенерацией)
task install-wire
# Запуск любого примера
go run ./cmd/<имя-примера>/
```
### Доступные task-команды
```bash
task install-wire # установить Wire локально в bin/
task generate-wire # сгенерировать wire_gen.go для cmd/wire-di/
task generate-wire-broken # попытка генерации с циклом (упадёт с ошибкой)
```