Go: graceful shutdown で処理中のリクエストが落ちるなら、main を Shutdown が終わるまで待たせる
srv.Shutdown(ctx) を呼ぶと、ListenAndServe は処理中のリクエストを待たずにすぐ http.ErrServerClosed を返す。それを見て main が終わると、処理中のリクエストもろともプロセスが消える。ErrServerClosed は正常終了として扱い、main は Shutdown 自体が戻るまで待つ。
よくある間違い
go
go func() {
<-ctx.Done() // SIGTERM
srv.Shutdown(context.Background())
}()
// Shutdown が呼ばれた瞬間にここが返り、main が終わる → 処理中のリクエストが切れる
if err := srv.ListenAndServe(); err != nil {
log.Fatal(err) // しかも ErrServerClosed をエラー扱いしている
}正しい形
go
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
srv := &http.Server{Addr: ":8080", Handler: mux, ReadHeaderTimeout: 5 * time.Second}
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("listen: %v", err) // 起動失敗などの本当のエラーだけ
}
}()
<-ctx.Done() // シグナルを待つ
stop() // 2 回目の Ctrl+C で即終了できるように
shutdownCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// ここで処理中のリクエストが終わるまでブロックする
if err := srv.Shutdown(shutdownCtx); err != nil {
log.Printf("shutdown: %v", err) // タイムアウトしたら残りは切られる
}
}- サーバーはゴルーチンで動かし、
mainは「シグナル →Shutdownが戻る」まで進まないようにするのが要点。 net/httpのドキュメントにも「Shutdownを呼ぶとServe/ListenAndServeはすぐErrServerClosedを返す。プログラムが終了せずShutdownの戻りを待つようにすること」と書かれている。
注意点
Shutdownは WebSocket など Hijack された接続は待たない。必要ならRegisterOnShutdownで通知して自前で閉じる。- Kubernetes などでは、SIGTERM の後もしばらくロードバランサから新しいリクエストが届くことがある。
Shutdownの前に数秒待つ(readiness を落としてから待つ)設定と組み合わせる。 shutdownCtxのタイムアウトは、オーケストレータの猶予時間(Kubernetes のterminationGracePeriodSeconds既定 30 秒など)より短くする。- タイムアウトの設定全般は Go: http.Server には必ず ReadHeaderTimeout を設定する。
出典
- @amitsinha__07 · 2026-10-20
- net/http ドキュメント
Server.Shutdown: pkg.go.dev