199 lines
9.8 KiB
Markdown
199 lines
9.8 KiB
Markdown
|
|
## 👋 Привет! Я Олег Козырев
|
|||
|
|
|
|||
|
|
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 # попытка генерации с циклом (упадёт с ошибкой)
|
|||
|
|
```
|