


Server.Shutdown прекращает приём новых соединений и ждёт завершения активных HTTP-запросов в пределах контекста. Важно, чтобы main дождалась этого ожидания. На Mentoring.digital разберём учебный сервер, который останавливается по сигналу и ограничивает завершение пятью секундами.
ListenAndServe выполняется в горутине. Буферизованный канал сообщает об ошибке запуска, чтобы занятый порт не оставил программу бесконечно ждать сигнала.
package main
import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
signalCtx, stop := signal.NotifyContext(
context.Background(), os.Interrupt, syscall.SIGTERM,
)
defer stop()
mux := http.NewServeMux()
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second)
fmt.Fprintln(w, "done")
})
server := &http.Server{
Addr: "127.0.0.1:8080", Handler: mux,
ReadHeaderTimeout: 3 * time.Second,
}
serveErr := make(chan error, 1)
go func() { serveErr <- server.ListenAndServe() }()
select {
case err := <-serveErr:
if !errors.Is(err, http.ErrServerClosed) {
log.Printf("server: %v", err)
}
return
case <-signalCtx.Done():
}
stop()
shutdownCtx, cancel := context.WithTimeout(
context.Background(), 5*time.Second,
)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err)
if closeErr := server.Close(); closeErr != nil {
log.Printf("close: %v", closeErr)
}
}
if err := <-serveErr; !errors.Is(err, http.ErrServerClosed) {
log.Printf("server: %v", err)
}
}
Контекст сигнала к этому моменту уже отменён. Если передать его в Shutdown, времени на спокойное завершение не останется. Поэтому пример создаёт новый контекст от Background с отдельным сроком ожидания.
При начале Shutdown вызов ListenAndServe возвращает ErrServerClosed. Это ожидаемый результат, но он ещё не означает, что активные запросы завершились. Именно поэтому ожидание Shutdown находится в main.
Когда срок контекста истекает, Shutdown возвращает ошибку. В примере после неё вызывается Close: это явный переход к принудительному закрытию, при котором оставшиеся запросы могут оборваться. Подберите срок по требованиям приложения, а ошибку сохраните в журнале.
Shutdown не ждёт завершения соединений, переданных через Hijack, например WebSocket. Для них, как и для фоновых задач приложения, нужен отдельный порядок остановки. Этот пример покрывает обычные HTTP-запросы.
Запустите сервер в тестовом проекте, начните запрос и отправьте сигнал прерывания, пока обработчик ждёт. Проверьте, завершился ли принятый запрос. Затем увеличьте задержку обработчика выше пяти секунд и проверьте ветку тайм-аута. Отдельно запустите сервер на уже занятом порту.
Управление жизненным циклом можно отработать на курсе Go, а остановку сервиса с несколькими зависимостями разобрать с ментором по Go.