DI контейнер конфигурации
Простой DI-контейнер на Go - минимализм в инициализации зависимостей без тяжелых фреймворков
В последнее время я часто использую простой DI-контейнер для инициализации зависимостей при запуске приложения. Такой подход иногда называют DI container, service locator или service provider. Он особенно удобен, когда хочется избежать тяжеловесных фреймворков вроде uber-go/fx или google/wire, но при этом сохранить читаемость и порядок в коде.
Разумеется, у такого «ручного» решения есть свои ограничения:
- Приходится завершать программу через
os.Exit(1)при ошибках инициализации - иначе сложно корректно обрабатывать ошибки на ранних этапах (включая вложенные зависимости). - Не поддерживает циклические зависимости - приложение завершиться при их наличии.
- Не является потокобезопасным: если контейнер используется из нескольких горутин одновременно, возможны гонки (race conditions).
Используйте этот подход, только если точно понимаете его ограничения и готовы их контролировать.
Почему это работает
Контейнер реализует ленивую инициализацию: зависимости создаются по требованию, но только один раз - при первом обращении.
Это позволяет:
- Избежать преждевременной загрузки ресурсов (например, соединения с БД).
- Четко выстроить порядок инициализации через зависимости методов (
Pool()→UsersRepo()и т.д.). - Писать компактный и локальный код без дополнительных зависимостей.
Пример реализации
| |
Использование в приложении
| |
Сам App может быть легковесной обёрткой - например, для graceful shutdown или логгирования. Всё «тяжелое» спрятано в контейнере.
Плюсы и минусы
| ✅ Плюсы | ❌ Минусы |
|---|---|
| Простота: всего ~50 строк кода | Нет поддержки циклических зависимостей |
| Никаких внешних зависимостей | Не thread-safe - не подходит для runtime-инжекции |
| Чёткий контроль над жизненным циклом | Требует ручного управления порядком инициализации |
Альтернативы
- google/wire - compile-time DI. Генерирует код. Нет рантайм-оверхеда, но требует настройки и понимания генерации.
- uber-go/fx - мощный фреймворк для DI и lifecycle-менеджмента. Подходит для больших приложений, но добавляет сложность.
- Конструкторы с явной передачей зависимостей - самый «чистый» подход (
server.New(pool, repo1, repo2, ...)), но при росте проекта вызовы становятся громоздкими.
Заключение
Такой простой контейнер - отличный выбор для большинства проектов. Главное - не забывать про его ограничения.