← Назад к вопросам
Graceful Shutdown HTTP сервера
2.0 Middle🔥 301 комментариев
#Основы Go#Сетевые протоколы и API
Условие
Реализуйте HTTP сервер с корректным завершением (graceful shutdown). Сервер должен корректно обрабатывать сигналы завершения и дожидаться завершения текущих запросов.
Требования
func startServer(addr string) error
- Запустить HTTP сервер на указанном адресе
- Перехватывать сигналы SIGINT и SIGTERM
- При получении сигнала - прекратить приём новых соединений
- Дождаться завершения текущих запросов (с таймаутом)
- Использовать context для контроля завершения
Подсказка
Используйте http.Server.Shutdown() и пакет signal.
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Решение
Анализ задачи
Требования:
- Запустить HTTP сервер
- Перехватывать SIGINT и SIGTERM
- Остановить приём новых соединений
- Дождаться завершения активных запросов
- Использовать context и timeout для корректного завершения
Стратегия:
- Запустить сервер в горутине
- Слушать сигналы в другой горутине
- При получении сигнала вызвать Shutdown() с таймаутом
Решение
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func startServer(addr string) error {
// Создаём HTTP сервер
server := &http.Server{
Addr: addr,
Handler: http.DefaultServeMux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
// Регистрируем хендлеры
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!\\n")
})
http.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
// Имитируем долгий запрос
time.Sleep(5 * time.Second)
fmt.Fprintf(w, "Slow response done\\n")
})
// Канал для ошибок сервера
serverErrors := make(chan error, 1)
// Запускаем сервер в горутине
go func() {
fmt.Printf("Server starting on %s\\n", addr)
serverErrors <- server.ListenAndServe()
}()
// Канал для обработки сигналов
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
// Ждём либо сигнала, либо ошибки сервера
select {
case sig := <-sigChan:
fmt.Printf("\\nReceived signal: %v\\n", sig)
case err := <-serverErrors:
if err != http.ErrServerClosed {
return fmt.Errorf("server error: %w", err)
}
}
// Graceful shutdown с таймаутом 30 секунд
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
fmt.Println("Gracefully shutting down server...")
if err := server.Shutdown(shutdownCtx); err != nil {
fmt.Printf("Shutdown error: %v\\n", err)
// Если timeout, то вынужденно закрываем
return server.Close()
}
fmt.Println("Server shut down successfully")
return nil
}
func main() {
if err := startServer(":8080"); err != nil {
fmt.Printf("Fatal error: %v\\n", err)
os.Exit(1)
}
}
Пояснения реализации
1. Создание сервера
server := &http.Server{
Addr: addr,
Handler: http.DefaultServeMux,
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
IdleTimeout: 60 * time.Second,
}
- Таймауты предотвращают зависание соединений
Handlerможет быть любойhttp.Handler
2. Запуск в горутине
go func() {
serverErrors <- server.ListenAndServe()
}()
- Сервер работает асинхронно
- ListenAndServe блокирует, поэтому нужна горутина
3. Перехват сигналов
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
- SIGINT: Ctrl+C
- SIGTERM: kill (default signal)
- Оба сигнала обрабатываются одинаково
4. Graceful shutdown
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(shutdownCtx)
Shutdown()останавливает приём новых соединений- Дождётся завершения текущих запросов (но не бесконечно)
- Если timeout — вернёт ошибку context.DeadlineExceeded
5. Fallback на Close
if err := server.Shutdown(shutdownCtx); err != nil {
return server.Close()
}
- Если даже shutdown не помогает, закрываем вынужденно
Альтернативный вариант: с управлением контекстом
func startServer(addr string) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
server := &http.Server{
Addr: addr,
Handler: http.DefaultServeMux,
BaseContext: func(net.Listener) context.Context {
return ctx
},
}
// Остальное как выше...
select {
case sig := <-sigChan:
fmt.Printf("Signal: %v\\n", sig)
cancel() // Отменяем контекст для всех handler'ов
}
// Затем Shutdown как раньше
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second)
defer cancelShutdown()
return server.Shutdown(shutdownCtx)
}
Плюс: handler'ы могут следить за отменой через context.
Полный пример с отслеживанием активных запросов
package main
import (
"context"
"fmt"
"net/http"
"os"
"os/signal"
"sync/atomic"
"syscall"
"time"
)
type Server struct {
http.Server
activeRequests atomic.Int32
}
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
s.activeRequests.Add(1)
defer s.activeRequests.Add(-1)
fmt.Printf("Request: %s %s (active: %d)\\n", r.Method, r.URL.Path, s.activeRequests.Load())
// Обрабатываем запрос
http.DefaultServeMux.ServeHTTP(w, r)
}
func main() {
server := &Server{
Server: http.Server{
Addr: ":8080",
ReadTimeout: 15 * time.Second,
WriteTimeout: 15 * time.Second,
},
}
server.Handler = server
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello\\n")
})
http.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(5 * time.Second)
fmt.Fprintf(w, "Done\\n")
})
// Запуск
go server.ListenAndServe()
// Ожидание сигнала
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
<-sigChan
fmt.Printf("\\nShutting down (active requests: %d)...\\n", server.activeRequests.Load())
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
server.Shutdown(ctx)
}
Производительность и безопасность
- Connection draining:
Shutdown()автоматически закрывает keep-alive соединения - Request timeout: handler'ы должны иметь свои таймауты
- Stuck requests: если request не завершится за 30 сек, соединение закроется force
- Clean exit: нет потери данных, все goroutine завершатся корректно