Как архитектурно решается Dependency Injection в Go
Что проверяют
Понимаете ли вы, что в Go DI обычно обходится без фреймворка и строится на конструкторах и маленьких интерфейсах. Интервьюер хочет увидеть, где у вас собирается граф зависимостей. Следом часто спрашивают, чем wire отличается от fx и когда контейнер вообще оправдан.
Как отвечать
Каркас из четырёх идей:
- конструкторы: зависимости передаются явно,
NewService(repo Repository, log *slog.Logger) *Service. Объект после конструктора полностью готов, поэтому сеттеры в Go не приживаются: они оставляют полусобранные структуры; - интерфейсы на стороне потребителя: пакет, которому нужно хранилище, сам объявляет
type OrderStore interface { Save(ctx context.Context, o Order) error }с одним или двумя методами. Реализация удовлетворяет ему неявно и про него не знает. Отсюда правило «принимай интерфейсы, возвращай структуры»; - корень композиции: весь граф собирается в
main(или вcmd/<app>) руками: конфиг, пул к базе, репозитории, сервисы, HTTP-хендлеры. Сорок строк очевидного кода читаются лучше любой магии; - функциональные опции: необязательные зависимости и настройки через
WithTimeout(...), чтобы не раздувать сигнатуру.
Когда граф разрастается, есть инструменты. google/wire генерирует обычный Go-код на этапе сборки, ошибка в графе видна при компиляции. uber-go/fx (поверх dig) собирает граф в рантайме через рефлексию и даёт хуки жизненного цикла OnStart/OnStop, но ошибки вылезают только при запуске.
Для тестов это означает простую подмену: в конструктор передаётся фейковая реализация интерфейса, контейнер для этого не нужен.
Где спотыкаются
Переносят подход из Java: интерфейс на двадцать методов объявляется рядом с реализацией «на будущее», а сверху контейнер ради контейнера.
Прячут зависимости в глобальные переменные, init() или context.Value. Контекст предназначен для данных запроса, а не для передачи репозитория.
Возвращают интерфейс из конструктора без нужды. Вызывающая сторона теряет конкретный тип, а если конструктор вернёт типизированный nil через интерфейс, проверка svc != nil окажется истинной.