テック技報最新号へ →
コード

Go: graceful shutdown で処理中のリクエストが落ちるなら、main を Shutdown が終わるまで待たせる

srv.Shutdown(ctx) を呼ぶと、ListenAndServe は処理中のリクエストを待たずにすぐ http.ErrServerClosed を返す。それを見て main が終わると、処理中のリクエストもろともプロセスが消える。ErrServerClosed は正常終了として扱い、main は Shutdown 自体が戻るまで待つ。

@amitsinha__07 · 投稿 2026-10-20
#go#http#graceful-shutdown#kubernetes

よくある間違い

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 を設定する。

出典

X の元の投稿を開く