贝利信息

Golang net http Server如何优雅关闭_HTTP服务关闭流程

日期:2026-01-13 00:00 / 作者:P粉602998670
必须用 Shutdown() 而不是 Close(),因为 Close() 会强制断开已接受但未响应的请求,而 Shutdown() 先拒新连、再等旧连完成或超时、最后强制关闭,配合 context.WithTimeout() 避免永久阻塞。

Go 的 http.Server 优雅关闭,核心就是用 Shutdown() 配合 context 超时,而不是 Close()os.Exit() —— 后两者会直接中断正在处理的请求,造成客户端收到 connection reset 或超时失败。

为什么必须用 Shutdown() 而不是 Close()

Close() 立即关闭 listener,所有新连接被拒,但**已接受但尚未响应的请求会被强制断开**;而 Shutdown() 会:

不加 context 超时的 Shutdown(context.Background()) 可能永久阻塞(比如 handler 里有 select{} 死循环),所以必须配 context.WithTimeout()

信号监听与 goroutine 启动顺序不能反

常见错误是

srv.ListenAndServe() 放在 main 协程里——它会阻塞,导致后续 signal.NotifyShutdown() 根本没机会执行。正确做法是:

注意:ListenAndServe() 返回 http.ErrServerClosed 是正常退出,不是错误,要显式忽略。

完整可运行示例(含耗时 handler 模拟 + 超时兜底)

package main

import ( "context" "fmt" "log" "net/http" "os" "os/signal" "syscall" "time" )

func main() { http.HandleFunc("/", func(w http.ResponseWriter, r http.Request) { log.Println("→ 开始处理请求(模拟 3s 业务)") time.Sleep(3 time.Second) fmt.Fprintln(w, "OK") log.Println("← 请求处理完成") })

srv := &http.Server{Addr: ":8080"}

// 启动服务(非阻塞)
go func() {
    log.Println("Server starting on :8080")
    if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
        log.Fatalf("server exited unexpectedly: %v", err)
    }
}()

// 监听系统信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Received shutdown signal")

// 执行优雅关闭(5s 超时)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
    log.Printf("Server forced to shutdown: %v", err)
} else {
    log.Println("Server gracefully stopped")
}

}

运行后按 Ctrl+C,你会看到:正在处理的请求走完才关,且不会超过 5 秒;如果 handler 卡死(如写死循环),也会在 5 秒后强制退出。真正的优雅,不是“等它自己停”,而是“给它机会停,但不无限等”。