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

Go 的 http.Server 优雅关闭,核心就是用 Shutdown() 配合 context 超时,而不是 Close() 或 os.Exit() —— 后两者会直接中断正在处理的请求,造成客户端收到 connection reset 或超时失败。
为什么必须用 Shutdown() 而不是 Close()?
Close() 立即关闭 listener,所有新连接被拒,但**已接受但尚未响应的请求会被强制断开**;而 Shutdown() 会:
- 立刻拒绝新连接(关闭 listener)
- 允许已建立的连接继续处理,直到完成或超时
- 主动调用每个活跃连接的
conn.Close()(如果未完成) - 等待所有连接进入 idle 状态,或 context 被 cancel
不加 context 超时的 Shutdown(context.Background()) 可能永久阻塞(比如 handler 里有 select{} 死循环),所以必须配 context.WithTimeout()。
信号监听与 goroutine 启动顺序不能反
常见错误是把 srv.ListenAndServe() 放在 main 协程里——它会阻塞,导致后续 signal.Notify 和 Shutdown() 根本没机会执行。正确做法是:
立即学习“go语言免费学习笔记(深入)”;
- 用
go func() { srv.ListenAndServe() }()异步启动服务 - 主 goroutine 立即注册信号:
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) - 阻塞等待信号:
,再触发Shutdown()
注意:ListenAndServe() 返回 http.ErrServerClosed 是正常退出,不是错误,要显式忽略。
完整可运行示例(含耗时 handler 模拟 + 超时兜底)
package mainimport ( "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 秒后强制退出。真正的优雅,不是“等它自己停”,而是“给它机会停,但不无限等”。
