Files
di-container/README.md
T
2026-04-13 08:14:09 +03:00

199 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 👋 Привет! Я Олег Козырев
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 # попытка генерации с циклом (упадёт с ошибкой)
```