2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00
2026-04-13 08:14:09 +03:00

👋 Привет! Я Олег Козырев

Staff Golang Engineer · Ex Ozon, Avito, Tinkoff

📺 YouTube · 💬 Telegram


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   # попытка генерации с циклом (упадёт с ошибкой)
S
Description
Олег Козырев
Readme 65 KiB
Languages
Go 100%