Запустить сервис с graceful shutdown и агрегированием ошибок очистки
Напишите запуск приложения, которое создаёт SendQueue, стартует на нём Service, затем ждёт SIGINT/SIGTERM. При фатальной ошибке — печать в stderr и выход со статусом 1. Очистка (Service.Stop, SendQueue.Close) должна выполняться на любом пути выхода, а ошибки очистки — сообщаться, а не глотаться.
type SendQueue struct{}
func NewSendQueue() (*SendQueue, error) { /* дано */ }
func (*SendQueue) Close() error { /* дано */ }
type Service struct{}
func StartService(*SendQueue) (*Service, error) { /* дано */ }
func (*Service) Stop() error { /* дано */ }
func main() {
// ваш код здесь
}
Допишите реализацию.
Паттерн func Main() error: main лишь делает if err := Main(); err != nil { log.Fatal(err) }. Внутри Main после каждой успешной инициализации регистрируйте defer, выполняющий очистку и вкладывающий её ошибку в именованный возврат через errors.Join. Defer-ы идут в LIFO, поэтому Service.Stop раньше SendQueue.Close. Это работает, ведь log.Fatal вызывает os.Exit, пропускающий defer-ы, поэтому они должны жить в Main, не в main.
- ✗Класть defer-ы в
main, гдеos.Exitизlog.Fatalих пропускает - ✗Глотать ошибки очистки вместо вкладывания их в возврат
- ✗Выполнять очистку в порядке инициализации, а не в обратном LIFO
- →Почему
log.Fatalвmainпропускает отложенную очистку, а возврат ошибки — нет? - →Как
errors.Joinдаёт одному завершению показать и runtime-ошибку, и ошибку закрытия?
Решение
func main() {
if err := Main(); err != nil {
log.Fatal(err) // печать в stderr + os.Exit(1)
}
}
func Main() (err error) {
q, err := NewSendQueue()
if err != nil {
return fmt.Errorf("init send queue: %w", err)
}
defer func() {
if e := q.Close(); e != nil {
err = errors.Join(err, fmt.Errorf("close send queue: %w", e))
}
}()
s, err := StartService(q)
if err != nil {
return fmt.Errorf("start service: %w", err)
}
defer func() {
if e := s.Stop(); e != nil {
err = errors.Join(err, fmt.Errorf("stop service: %w", e))
}
}()
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
<-sigs
return nil
}
Почему func Main() error. log.Fatal вызывает os.Exit, а os.Exit не выполняет отложенные defer-ы. Если положить defer s.Stop() прямо в main и затем вызвать log.Fatal, очистка не сработает. Вынеся логику в Main, мы возвращаем ошибку, defer-ы отрабатывают на выходе из Main, и только потом main вызывает log.Fatal.
Порядок и агрегирование. Defer-ы исполняются в LIFO: сначала s.Stop(), затем q.Close() — обратно порядку инициализации (сервис зависит от очереди). Именованный возврат err плюс errors.Join позволяют сообщить и основную ошибку, и ошибки очистки, не потеряв ни одной.