Комментарии 23
такой код мне совсем не радует глаз,
должно быть только 1 строчка кода для подключения (+отключение) куда угодно,
например так:
func main() {
stopapp.StartWaitStop()
mssql_gorm.StartDB()
postgres_gorm.StartDB()
nats.StartNats()
camunda.StartCamunda()
liveness.Start()
stopapp.Wait_GracefulShutdown()
}
как в моём стартере:
https://github.com/ManyakRus/starter
Красиво. Но где здесь внедрение зависимостей? Тут запуск каких-то абсолютно независимых друг от друга компонент.
Мой пример кода делает всё то же самое,
что и ваш огромный main,
а именно:
Подключение к PostgreSQL
Подключение REST
Подключение Liveness (CheckHealth)
Отключение с Graceful Shutdown.
Вы напрасно создаёте слишком много кода и зависимостей.
Вы не ответили на вопрос. В вашем примере кода нет внедрения зависимостей.
Здесь есть только одна зависимость: подключение к PostgreSQL желательно делать раньше чем подключение REST, что и учитывается в моём коде.
Больше нет тут никаких зависимостей, и не должно быть.
Все зависимости только в вашей голове - чтобы усложнить самому себе жизнь.
А как писать тесты без зависимостей? )
Александр, мне кажется, основная цель вашего комментария - реклама собственного проекта... ((
о боги, я посмотрел гитхаб "примера". Сразу мысль - что это за ужасный Java-стиль. Полистал другие репы автора и не ошибся.
Я не представляю как с этим вообще удобно работать, что в IDE что точечно в гитхабе. По ресурсам все тоже печально, можно было бы не заморачиваться с единым main а просто прописывать init() в ваших "модулях" и все. Один хрен никакого внешнего взаимодействия меж модулями нет, только прямые инициализации
Спасибо за конструктивную, аргументированную критику, за добрые слова! И вам не хворать! ))
Ну вот инит это такое себе как мне кажется. Это синглетоны, причём неявные. Они почти всегда зло. В определённых кругах существует мнение, что создаваться все должно явно, неявное создание есть зло, а объект должен быть готов к использованию сразу после завершения конструктора. Тогда все эти проблемы исчезают. У вас в любом месте все готово что вы создали и вам передали, и никаких проблем более не будет, кроме случая явной ошибки вызова rpc.
sqlSet, err := sqlset.New(queries.QueriesFS)
if err != nil {
return nil, fmt.Errorf("failed to load queries: %v", err)
}
a.repository = postgres.New(sqlsetpgxhelper.New(sqlSet))
a.usecase = usecase.New(a.repository)А что если в этом примере не пихать сразу поле в структуру а создать переменную, а в конце функции вернуть заполненный App, у которого вызвать метод Run скажем который запустить горутинки с серверами и брокерами?
Я в начале статьи об этом и писал. В реальности получается main на 6 экранов и больше. Читается плохо, модифицируется плохо.
Если сервис совсем простенький (как в моём примере - HTTP + БД) - да, можно не городить огород а делать всё как обычно, как вы и описали.
Но вот сейчас я работаю не над микросервисом, а над монолитным проектом на Go и тут всё пихать в main - самоубийственно.
Ну вынесите из main в другой пакет)
Если прям монолит монолит, то напишите обертку для репозиториев брокеров и сервисов)
Да без проблем. А что если всё-таки "пихать поле в структуру"? )) Что плохого в моём подходе-то? Мне он кажется более удобным. Он полностью снимает заботу о порядке создания зависимостей. И он позволяет максимально легко их добавлять.
Но, безусловно, можно всё сделать и без него, традиционным способом. Код main получится побольше, читаться и правиться будет чуть хуже. Зато просто и без ухищрений.
Тут уже дело вкуса, ИМХО.
Добрый день, не понимаю а почему не использовали fx: https://github.com/uber-go/fx
Там из коробки есть fx.Lifecycle с хуками OnStart/OnStop, контролируемый порядок запуска зависимостей, graceful shutdown по сигналам, и health checks. По сути, то что в статье выстраивалось через lazy singleton + sync.Once. Аргумент про «философию языка» очень спорный fx активно используется в продакшене в самом Uber и в других крупных компаниях ( 7.4k stars говорит само за себя)
давно перешли на него благодаря удобство и простоте, есть app.go куда подключается модули.
func NewApp(
cfg *config.AppConfig) fx.Option {
return fx.Module("app",
NewAPI(cfg),
NewStorages(cfg),
NewProducerQueue(cfg),
NewUseCases(cfg),
fx.Invoke(telemetry.NewTracer),
fx.Invoke(metrics.RunPrometheusMetrics),
fx.Invoke(StartServer),
)
}И вот так выглядит main
...
application := fx.New(app.NewApp(&cfg))
if err = application.Err(); err != nil {
logger.Error("running application failed", zap.Error(err))
return
}
application.Run()
...
P.S. не хотел обидеть, интересная статья, интересный ход мыслей по ходу статьи, но к концу как будто бы было написано половина fx)))
Спасибо за ссылку и добрые слова! Не использовал fx по двум причинам - конкретно про этот фреймворк не знал и "данный подход не соответствует философии языка: простота, читаемость, отсутствие скрытой логики." - это я в начале упомянул.
На самом деле, будь моя воля, я бы использовал fx. Но в команде есть тимлид и другие разработчики и у них мнение, увы, не всегда совпадает с моим.
Кроме того цель данной статьи помимо прочего - познавательная. И если бы я просто написал - "берите fx и не думайте", это было бы не интересно. Велосеипеды в боевой разработке изобретать не нужно, но крайне полезно для профессионального развития.
Обычно против DI фреймворков выдвигают два аргумента - магия это зло и - зачем тащить лишнюю зависимость. Но как по мне - fx калссный (uber вроде плохого не делает) и я бы использовал его. Ща пойду пробовать. Еще раз сапсибо за ссылку!
Потому что только при запуске будет видно, что есть проблема с DI и километром трейсов без явной причины, и останется пальцем в небо тыкать что там рефлексия не нашла?)
только при запуске будет видно
Есть же простейший тест, который покажет каких зависимостей не хватает.
func TestNewApp(t *testing.T) {
type args struct {
cfg *config.AppConfig
logger *zap.Logger
}
tests := []struct {
name string
args args
want *fx.App
}{
{
name: "default",
args: args{
cfg: &config.AppConfig{},
logger: zap.NewNop(),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if err := fx.ValidateApp(NewApp(tt.args.cfg, tt.args.logger)); err != nil {
t.Errorf("NewApp() error = %s", err)
return
}
})
}
}
километром трейсов без явной причины
Если ошибка в рантайме, да сам трейс длинный, но конкретная ошибка всегда будет в последней строчке.
NewPostgresConnector.postgres.WithInstance: failed to connect to user=*** database=mydb: 127.0.0.1:54321 (127.0.0.1): dial error: dial tcp 127.0.0.1:54321: connect: connection refused"Скрытый текст
2026-02-24T08:32:59.423+0300 error cmd/main.go:51 running application failed {"error": "could not build arguments for function "project/internal/infrastructure/api/http/login".Router (/path/to/project/internal/infrastructure/api/http/login/router.go:24): failed to build usecases.LoginUsecase: could not build arguments for function "project/internal/domain/usecases".NewLoginUsecase (/path/to/project/internal/domain/usecases/login.go:34): failed to build usecases.AuthStorage: could not build arguments for function "reflect".makeFuncStub (/path/to/sdk/go1.23.5/src/reflect/asm_arm64.s:29): failed to build connectors.PostgresConnector: received non-nil error from function "project/pkg/connectors".NewPostgresConnector (/path/to/project/pkg/connectors/postgres.go:44): NewPostgresConnector.postgres.WithInstance: failed to connect to user=*** database=mydb: 127.0.0.1:54321 (127.0.0.1): dial error: dial tcp 127.0.0.1:54321: connect: connection refused"}

За чистую main