Go服务需手动监听SIGINT/SIGTERM信号并调用http.Server.Shutdown配合超时context,确保请求优雅终止;忽略http.ErrServerClosed,所有IO操作须支持ctx取消。

Go 服务启动后按 Ctrl+C 就 panic?http.Server 缺少关闭逻辑
默认的 http.ListenAndServe 是阻塞式调用,没有内置退出信号监听。一旦收到 SIGINT(Ctrl+C)或 SIGTERM,进程直接终止,正在处理的 HTTP 请求被粗暴中断,连接可能重置、数据写一半就丢。
必须手动监听系统信号,并触发 http.Server 的 Shutdown 方法——它会先关闭监听器,再等待已有请求完成(或超时),这才是“优雅”的核心。
- 别用
os.Exit(0)或直接 return,这等于放弃所有未完成请求 -
Shutdown是阻塞的,需配合context.WithTimeout控制最大等待时间,避免卡死 - 务必在调用
Shutdown前停止接收新连接,否则新请求仍会进来
怎么写一个带超时的 Shutdown 流程?
关键不是“关服务器”,而是“给正在跑的请求留出收尾时间”。典型做法是:起一个 goroutine 启动服务,主线程监听信号,收到后调用 srv.Shutdown 并传入带超时的 context.Context。
示例片段:
立即学习“go语言免费学习笔记(深入)”;
srv := &http.Server{Addr: ":8080", Handler: myHandler}
go func() {
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatal(err)
}
}()
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
<-sig // 阻塞等信号
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("server shutdown error: %v", err)
}
-
http.ErrServerClosed是正常关闭时ListenAndServe返回的错误,不是异常,要忽略 - 超时时间(如
10*time.Second)得根据业务实际设:API 接口可短些,文件上传或长轮询就得更长 - 别漏掉
defer cancel(),否则 context 泄露
为什么加了 Shutdown 还有请求失败?
常见原因是中间件或 handler 里用了不可中断的阻塞操作,比如没设超时的 http.Client.Do、死循环、或同步写磁盘。这些不会响应 context.Context 的取消信号,导致 Shutdown 等待超时后强制退出。
- 所有外部 HTTP 调用必须用带 cancel 的
context:client.Do(req.WithContext(ctx)) - 数据库查询也要传
ctx,例如db.QueryContext(ctx, ...) - 避免在 handler 里用
time.Sleep或无条件for {},改用select+ctx.Done()检查退出 - 日志库如果异步刷盘,确认它支持 graceful flush,否则最后一段日志可能丢失
HTTP/2 和 TLS 场景下要注意什么?
如果启用了 HTTPS 或 HTTP/2,http.Server 的行为基本一致,但有两个细节容易被忽略:
-
srv.TLSConfig如果是动态生成的(比如用autocert),确保其GetCertificate不会阻塞,否则Shutdown可能卡在 TLS 握手阶段 - HTTP/2 的流复用会让单个 TCP 连接承载多个请求,
Shutdown会等所有流自然结束,但若客户端不主动关闭连接,可能拖慢整体退出——此时靠超时兜底更关键 - 别把
http.Server和http.ServeTLS混用;用srv.ListenAndServeTLS才能统一走Shutdown流程
真正麻烦的从来不是写几行 Shutdown,而是整个调用链是否都尊重 context 取消。一个没处理 ctx.Done() 的 goroutine,就能让服务卡在退出前最后一秒。


















