Пролог
Для начала давайте определимся почему стоит стремится использовать DI (Dependency injection) в своих сервисах.
Когда мы создаем новый сервис у нас достаточно небольшой объем кода, зависимости которого мы можем напихать в своеобразный хаб, где ручками прописать все инициализации, хранить ссылки в структуре и дальше разными методами доставать их оттуда для инициализации других кусков программы.
Основная проблема в таком подходе заключается в следующем:
программисту всегда нужно обращаться к такой структуре,
держать ее у себя в голове (а она с ростом сервиса все дальше и дальше обрастает зависимостями)
понимать в какой последовательности нужно инициализировать те или иные компоненты системы
любое расширение компонентов приводит к рефакторингу большого объема кода
а самая боль, это то что со временем код превращается в такой кусок лапши что время на понимание структуры сервиса превалирует над процессом написания нового кода (-:
Что такое DI контейнер?
По своей сути DI контейнер это кусок программы или библиотека которая понимает как устроен ваш сервис, какие компоненты он содержит и как их инициализировать и в какой последовательности, уже в его ответственность входит понимание как правильно запустить ваш сервис в рабочие состояние.
Такой подход позволяет:
освободить голову от хранения структуры программы
упростить читаемость кода
перейти к процессу реализации новых фич в сервисе без знания всего проекта в целом
Как реализовать свой DI контейнер
Для начала давайте представим весь наш сервис в виде графа. Где место откуда выходит стрелка это исходящий параметр у компонента, то на что указывает стрелка указывает стрелка это входящий параметр у компонента.

К примеру у нас простой сервис, который по запросу отдает из базы данных какой то контент только для авторизованных пользователей. Для анализа графа такого сервиса можно рассмотреть один из алгоритмов топологической сортировка - алгоритм Кана пример реализации алгоритма можем посмотреть здесь.
Используя его давайте представим наши зависимости:
Компонент | Зависимость | Результат инициализации |
HttpServer | *Auth, *Logger, *Content | error - будем возвращать в случае если запуск не удался |
Auth (будем использовать как middleware для проверки входящего запроса) | *Database | *Auth |
Logger (для логирования всех запросов) | --- | *Logger |
Content | *Database | *Content |
Database (наш источник знаний о контенте и о юзерах) | --- | *Database |
Благодаря такому анализу графа DI понимает что за чем инициализировать, может сохранить у себя структуру зависимостей и начать их инициализировать: первым что мы начнем создавать с помощью DI это Database и Logger т.к. для их создания не требуется чего-то дополнительного, далее создаем Auth и Content т.к. зависимость Database у нас уже создана и в конце создаем HttpServer, т.к. у него самое большое кол-во зависимостей и они уже созданы.
На что стоит обратить внимание при разработки DI контейнера:
при сборе информации о графе учитывать не только название компонентов но и их полный адрес в сервисе
учитывать возможные циклические зависимости, при их нахождении DI не должен давать запускать сервис а выдавать ошибку
должен уметь принимать не только методы инициализации но и уже созданные объекты, слайсы, каналы и т.п., что позволит вам руками создавать необходимые части приложения (если очень нужно)))
P.S.
Пример реализации вышеупомянутой механики можно рассмотреть на примере библиотеки https://github.com/deweppro/go-app которая может быть каркасом для ваших сервисов.
P.S.S.
Буду рад комментариям и пулреквестам по оптимизации библиотеки ;-)
Всем удачи!