直接调用srv.shutdown()后连接仍断开,因其仅关闭http层连接,对websocket、sse等升级后的长连接无感知;这些连接需手动管理并响应ctx.done(),否则会被强制中断。

为什么直接调用 srv.Shutdown() 后连接还是断了
因为 Shutdown() 只管 HTTP 层连接,对已升级的长连接(如 WebSocket、SSE)完全无感知。这些连接脱离了 http.Server 管理,底层 TCP 连接仍在跑,但 Shutdown() 不会等它们自然结束。
常见现象是客户端收到 connection reset by peer 或大量 CLOSE_WAIT,不是 Shutdown() 失效,而是它根本没覆盖到那些连接。
- WebSocket 升级后必须手动存进
sync.Map,否则 shutdown 信号来了也找不到对象去Close() - SSE handler 里没监听
req.Context().Done(),响应流会卡住,超时后被强制中断 - 超时设太短(比如 5s),大文件上传或流式响应直接被强杀,不是连接“丢”,是被提前终止
如何让新进程拿到老 listener 的 fd
Linux 下不能复制 fd,只能靠 fork + exec 时继承,且必须显式传递——Go 标准库不自动做这事。
关键三步缺一不可:
- 父进程调
ln.(*net.TCPListener).File()拿到*os.File,记下file.Fd()(比如是 3) - 启动子进程时,
os.StartProcess的sys.ProcAttr.Files必须填入:[]uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd(), 3} - 子进程启动后第一件事:读环境变量(如
os.Getenv("LISTEN_FD")),用os.NewFile(3, "")恢复*os.File,再调net.FileListener(file)转成net.Listener
验证是否成功:lsof -i :8080 应同时看到两个 PID;若只看到一个,说明 fd 传递失败或子进程没调 net.FileListener。
在 Golang 中使用 samber/hot 进行内存缓存,支持 LRU、LFU、TinyLFU、W‑TinyLFU、S3FIFO、ARC、TwoQueue、SIEVE、FIFO 等淘汰算法,提供 TTL、缓存加载器及分片功能。
handler 里哪些地方必须检查 ctx.Done()
只要可能阻塞的操作,都得主动响应取消信号,否则 Shutdown() 会一直等,直到超时强杀 goroutine。
- 数据库查询:用
db.QueryContext(ctx, sql),别用db.Query() - HTTP 客户端请求:用
http.NewRequestWithContext(ctx, ),再传给client.Do() - 定时等待:不用
time.Sleep(),改用select { case - WebSocket 读写:在
conn.ReadMessage()和conn.WriteMessage()前加select { case ,或用 <code>conn.SetReadDeadline()/SetWriteDeadline()配合 - 中间件里启动的 goroutine(如日志上报、指标采集):必须监听
ctx.Done()并 clean up
为什么推荐 facebookgo/grace 而不是自己手撸
它把监听 socket 的传递、子进程启动、父进程等待退出全封装进 gracehttp.Serve,不需要手动处理 SO_REUSEPORT 或 FD_CLOEXEC,默认用 SIGUSR2 触发,且子进程通过 os.StartProcess 显式继承监听 fd。
手写容易漏的关键点:
- 忘记调
syscall.CloseOnExec(fd, true),导致子进程启动后监听 fd 被自动关闭,报错listen tcp :8080: bind: address already in use - 没设
ReadTimeout/WriteTimeout,父进程 hang 在 Accept 或长连接上,超时前不会退出 - 监听多个端口(HTTP + HTTPS + metrics)时,只传单个
*http.Server,漏掉其他 listener
真正难的不是“怎么传 fd”,而是确保父子进程共存期间不争抢 accept、旧连接有足够时间自然关闭、所有 goroutine 都响应 ctx —— 这些边界条件,手写极易遗漏。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










