功能说明
main 用 signal.NotifyContext 把中断信号转换为 context,在 127.0.0.1:0 上启动 http.Server,并并发发起一个耗时 300ms 的请求。50ms 后调用 stop 模拟收到 SIGTERM,再用带 2 秒超时的 context 执行 Shutdown。Shutdown 会停止监听、等待进行中的 handler 完成,最后通过 serveErr 区分正常关闭与真实错误。
关键代码
Serve 在独立 goroutine 中运行,主流程不会被阻塞。server 设置了 ReadHeaderTimeout 与 IdleTimeout,减少慢速连接占用。Shutdown 返回后检查 http.ErrServerClosed,这是 Serve 在正常关闭时的预期错误。clientDone 确保客户端读取完成后再退出程序,避免日志丢失。示例用 stop() 代替真实信号,便于在测试和本地直接运行。
使用注意
Shutdown 只等待已经进入的请求,不会中断它们;超过 grace period 后应调用 server.Close 强制关闭。负载均衡器需要先摘除实例,否则停机期间仍可能收到新请求。生产环境还应关闭后台 worker、数据库连接池和消息消费者,并为整个关闭流程设置统一预算。Windows 下信号语义不同,容器中通常收到 SIGTERM,本地调试可能是 Ctrl+C。
验证方式
运行程序可看到客户端收到 finished,随后输出已停止接收新连接并排空请求。把 handler 延迟改为 5 秒、grace period 保持 2 秒,可验证 Shutdown 返回 deadline exceeded。再用真实进程发送 SIGTERM,或在测试中取消 NotifyContext 的父 context,检查 Serve 返回 ErrServerClosed 且没有 goroutine 泄漏。
代码示例
package main
import (
"context"
"errors"
"fmt"
"io"
"log"
"net"
"net/http"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
mux := http.NewServeMux()
mux.HandleFunc("/slow", func(w http.ResponseWriter, r *http.Request) {
select {
case <-time.After(300 * time.Millisecond):
fmt.Fprintln(w, "finished")
case <-r.Context().Done():
log.Printf("client went away: %v", r.Context().Err())
}
})
mux.HandleFunc("/healthz", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
io.WriteString(w, "ok")
})
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
log.Fatal(err)
}
server := &http.Server{
Handler: mux,
ReadHeaderTimeout: 2 * time.Second,
IdleTimeout: 30 * time.Second,
}
serveErr := make(chan error, 1)
go func() {
serveErr <- server.Serve(listener)
}()
clientDone := make(chan struct{})
go func() {
defer close(clientDone)
resp, err := http.Get("http://" + listener.Addr().String() + "/slow")
if err != nil {
log.Printf("request error: %v", err)
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
log.Printf("client got %q", body)
}()
time.Sleep(50 * time.Millisecond)
stop() // Simulates the signal after the request is already in flight.
shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
defer cancel()
if err := server.Shutdown(shutdownCtx); err != nil {
log.Printf("graceful shutdown failed: %v", err)
} else {
log.Println("stopped accepting new connections and drained in-flight work")
}
<-clientDone
if err := <-serveErr; err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Printf("serve error: %v", err)
}
}