Назад
Golang

Context и graceful shutdown в Go

Как не терять запросы и не плодить утечки горутин

Как не стоит завершать приложение:

sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig
server.Close()   // ← все запросы в полёте умирают

Последствия:

  • клиенты получают 499 / EOF / таймаут
  • горутины висят или паникуют
  • в Kubernetes под застревает в TERM → SIGKILL через grace period

Современный подход (Go 1.16+)

func main() {
    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    srv := &http.Server{
        Addr:    ":8080",
        Handler: router,
        BaseContext: func(net.Listener) context.Context {
            return ctx  // все новые запросы получают контекст, который отменится при SIGTERM
        },
    }

    // Запускаем сервер в отдельной горутине
    go func() {
        if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
            log.Printf("HTTP server error: %v", err)
        }
    }()

    // Ждём сигнал
    <-ctx.Done()
    log.Println("Shutdown signal received")

    // Даём время на завершение запросов (25–30 секунд — типичный таймаут)
    shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
    defer cancel()

    if err := srv.Shutdown(shutdownCtx); err != nil {
        log.Printf("Graceful shutdown error: %v", err)
        // Опционально: srv.Close() для жёсткого завершения
    }

    log.Println("Server stopped gracefully")
}

Ключевые моменты, которые спасают от утечек

  1. Передавайте ctx во все длительные операции

    • БД: QueryContext, ExecContext
    • Redis: Do(ctx, cmd)
    • gRPC: ctx в каждом вызове
    • Свои горутины: select { case <-ctx.Done(): return }
  2. Readiness / Liveness в Kubernetes
    Как только получили сигнал → /readyz → 503 → kube перестаёт слать трафик.

  3. Фоновые воркеры
    Используйте sync.WaitGroup + общий ctx:

     var wg sync.WaitGroup

   for i := 0; i &lt; 5; i++ {
       wg.Add(1)
       go func() {
           defer wg.Done()
           for {
               select {
               case &lt;-ctx.Done():
                   return
               case job := &lt;-queue:
                   process(job)
               }
           }
       }()
   }

   // В shutdown: после srv.Shutdown() делаем wg.Wait()

Что стоит учитывать

  • signal.NotifyContext → глобальный appCtx
  • BaseContext в http.Server
  • srv.Shutdown(with timeout)
  • Все длительные операции обязательно используют context
  • sync.WaitGroup / errgroup для фоновых воркеров

Будте здоровы и не забывайте про контекст и длительные операции🙃