Launch a service with graceful shutdown and aggregated cleanup errors
Write the startup for an app that creates a SendQueue, starts a Service on it, then waits for SIGINT/SIGTERM. On a fatal error print to stderr and exit status 1. Cleanup (Service.Stop, SendQueue.Close) must run on every exit path and any cleanup errors must be reported, not swallowed.
type SendQueue struct{}
func NewSendQueue() (*SendQueue, error) { /* given */ }
func (*SendQueue) Close() error { /* given */ }
type Service struct{}
func StartService(*SendQueue) (*Service, error) { /* given */ }
func (*Service) Stop() error { /* given */ }
func main() {
// your code here
}
Write the implementation.
Use the func Main() error pattern: main just does if err := Main(); err != nil { log.Fatal(err) }. Inside Main, after each successful init register a defer that runs cleanup and folds any cleanup error into the named return with errors.Join. Defers run LIFO, so Service.Stop runs before SendQueue.Close. This works because log.Fatal calls os.Exit, which skips defers, so they must live in Main, not main.
- ✗Putting defers in
main, wherelog.Fatal'sos.Exitskips them - ✗Swallowing cleanup errors instead of joining them into the return
- ✗Running cleanup in init order rather than LIFO reverse order
- →Why does
log.Fatalinmainskip deferred cleanup, but returning an error does not? - →How does
errors.Joinlet one shutdown surface both a runtime error and a close error?
Solution
func main() {
if err := Main(); err != nil {
log.Fatal(err) // print to 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
}
Why func Main() error. log.Fatal calls os.Exit, and os.Exit does not run pending defers. If you place defer s.Stop() directly in main and then call log.Fatal, the cleanup never runs. Moving the logic into Main, we return an error, the defers fire as Main unwinds, and only then does main call log.Fatal.
Order and aggregation. Defers execute LIFO: first s.Stop(), then q.Close() — the reverse of init order (the service depends on the queue). The named return err plus errors.Join let you report both the primary error and any cleanup errors without losing any.