Чистим main.go: предсказуемый старт и надежный Graceful Shutdown
- вторник, 16 декабря 2025 г. в 00:00:09
Сталкивались ли вы с болью при управлении порядком запуска и остановки зависимостей в вашем Go-сервисе?
Разработка больших сервисов неизбежно приводит к необходимости управлять множеством зависимостей. В этом контексте мы говорим о долгоживущих компонентах, чья работа обеспечивается отдельными горутинами: как правило, это блокирующий метод (например, Start), внутри которого крутится цикл обработки.
Примерный сценарий жизненного цикла сервиса выглядит так:
При запуске критически важно, чтобы пул соединений с БД, кэш и очереди были полностью готовы до того, как HTTP-сервер откроет порт и начнет принимать входящий трафик.
С graceful shutdown ситуация обратная: порядок должен быть строго зеркальным. Сначала нужно перестать принимать новые запросы, дождаться завершения текущих, остановить воркеры, и только потом разрывать соединения с инфраструктурой. Иначе мы получаем неприятные ошибки подключения и даже потерянные транзакции в момент деплоя.
Если эти проблемы вам не знакомы, смело закрывайте вкладку. Скорее всего, эта статья не принесет вам пользы.
Но если вы ищете способ автоматизировать эту рутину, сохранив код чистым - добро пожаловать под кат.
Чаще всего разработчики идут по пути ручного связывания. Вы пишете код инициализации прямо в main, используя errgroup и много defer. Это решение отлично работает на малых масштабах, но с ростом проекта становится хрупким: стоит поменять местами два defer (например, закрытие базы и остановку воркера), как ваш graceful shutdown становится не таким и изящным.
Второй путь - DI-фреймворки (например, Uber Fx). Они действительно управляют зависимостями за вас. Но мало того, что такие инструменты, как правило, обладают весьма спорным синтаксисом, так они еще и «пролезают» по всему коду, делая его зависимым от конкретной библиотеки.
Как сохранить явное связывание зависимостей, но полностью избавиться от ручного управления порядком старта и остановки? Идея проста: оставить инициализацию явной, но полностью автоматизировать порядок выполнения.
Небольшое уточнение перед примерами. Визуализация, которую вы увидите ниже - это отдельный проект, который лежит в репозитории библиотеки. Он отображает работу на синтетическом наборе компонентов и позволяет интерактивно посмотреть, как goscade управляет запуском, остановкой и различными сценариями. Этот инструмент удобен для понимания и экспериментов, но не является частью ядра библиотеки и никак не влияет на её работу в ваших приложениях.

Вы инициализируете структуры и передаете в них зависимости, как обычно. Никаких специальных реестров или сложной конфигурации.
Библиотека берет эти структуры и автоматически строит граф зависимостей, анализируя их поля через рефлексию.
Компоненты запускаются строго в топологическом порядке (от независимых к зависимым).
При остановке гарантируется корректный обратный порядок.
В итоге ваш доменный код остается чистым и мало что знает о библиотеке, а main.go избавляется от хрупкой логики управления горутинами.
Важно уточнить: рефлексия используется ровно один раз - на этапе инициализации приложения для построения графа. На производительность работающего сервиса это никак не влияет.
package main
import (
"context"
"log"
"github.com/ognick/goscade/v2"
)
// 1. Database - независимый компонент
type Database struct{}
func (d *Database) Run(ctx context.Context, ready func(error)) error {
// Здесь происходит реальное подключение к БД
ready(nil) // Сигнал: "Готов, можно запускать зависимые компоненты"
<-ctx.Done() // Ждем до сигнала остановки
// Закрытие соединений...
return nil
}
// 2. Server - зависит от Database
type Server struct {
DB *Database // <--- Goscade увидит эту связь через рефлексию
}
func (s *Server) Run(ctx context.Context, ready func(error)) error {
// Этот код выполнится ТОЛЬКО после того, как Database вызовет ready(nil)
ready(nil) // Сигнал: "Сервер принимает трафик"
<-ctx.Done() // Работаем до сигнала остановки
// Остановка...
return nil
}
func main() {
// Инициализируем Lifecycle менеджер
lc := goscade.NewLifecycle(log.Default(), goscade.WithShutdownHook())
// Создаем компоненты
db := &Database{}
server := &Server{DB: db} // Явно передаем зависимость
// Регистрируем (порядок не важен)
goscade.Register(lc, server)
goscade.Register(lc, db)
// Запускаем
goscade.Run(context.Background(), lc, func() {
log.Println("Все компоненты запущены!")
})
}Как вы видите, единственное, что навязывает требуется от зависимостей - имплементировать метод Run:
Run(ctx context.Context, readinessProbe func(error)) error
Вы вызываете readinessProbe(nil), когда компонент полностью готов к работе.
Если же на этапе старта произошла ошибка - передаете её в коллбек, и библиотека корректно прервет запуск всей цепочки.

Библиотека продолжает следить за компонентами и после успешного старта.
Если в процессе работы метод Run любого из них завершится с ошибкой, весь граф будет остановлен в правильной обратной последовательности
Кроме того, библиотека берет на себя рутину, которую обычно приходится копипастить из проекта в проект:
Перехват сигналов ОС: Опция WithShutdownHook() автоматически подписывается на SIGINT и SIGTERM. Вам не нужно создавать каналы для os.Signal вручную.
Таймауты на старт и стоп: Вы можете ограничить время на инициализацию или завершение приложения, чтобы зависший драйвер не вешал деплой бесконечно.
Управление через контекст: Вся работа построена на стандартном context.Context. Если вы отмените родительский контекст, вся цепочка сервисов корректно остановится.
Забирайте в свои проекты, экспериментируйте. Если найдете баги или придумаете, как сделать лучше - велкам в Issues и PR 👉 github.com/ognick/goscade