必须用 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) - 阻塞等待信号:
<-quit,再触发Shutdown()
注意:ListenAndServe() 返回 http.ErrServerClosed 是正常退出,不是错误,要显式忽略。
完整可运行示例(含耗时 handler 模拟 + 超时兜底)
package main
<p>import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)</p><p>func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r <em>http.Request) {
log.Println("→ 开始处理请求(模拟 3s 业务)")
time.Sleep(3 </em> time.Second)
fmt.Fprintln(w, "OK")
log.Println("← 请求处理完成")
})</p><pre class='brush:php;toolbar:false;'>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 秒后强制退出。真正的优雅,不是“等它自己停”,而是“给它机会停,但不无限等”。










