必须弃用http.listenandserve,改用显式http.server实例配合文件描述符继承复用socket;因listenandserve不暴露server导致无法shutdown、设超时,且其返回错误即表明监听失败,须立即终止程序而非静默退出。

直接忽略 http.ListenAndServe 的返回错误,是服务启动失败却“静默退出”的根本原因;而平滑重启不是靠换端口或重试,必须绕过 ListenAndServe,用 http.Server 实例 + 文件描述符继承来复用 socket。
为什么 ListenAndServe 返回 error 就该立刻终止程序
http.ListenAndServe 是阻塞函数,但它不是“永远不返回”——只要端口不可用(被占、权限不足、TIME_WAIT 未清),它会立即返回非 nil 错误,比如 listen tcp :8080: bind: address already in use。此时若没检查 err,进程就直接退出,连日志都不打。
- 永远用
if err := http.ListenAndServe(...); err != nil { log.Fatal(err) }包裹,别省略 - 错误不是“异常”,是关键运行状态:它告诉你监听根本没开始,后续所有 handler 都不会被调用
- 开发时可加
lsof -i :8080快速确认是否真被占;生产环境应避免依赖SO_REUSEADDR来掩盖问题
平滑重启必须弃用 ListenAndServe,改用 http.Server 实例
http.ListenAndServe 内部创建了匿名 http.Server,但不暴露实例,导致你无法调用 Shutdown()、设超时、传 TLS config,更没法在信号到来时优雅停服。
- 定义显式变量:
srv := &http.Server{Addr: ":8080", Handler: mux, ReadTimeout: 5 * time.Second, WriteTimeout: 10 * time.Second} - 启动用
srv.ListenAndServe(),但注意它仍会阻塞;若需并发执行其他逻辑(如 metrics 上报),得套go srv.ListenAndServe() -
Shutdown()不等于Close():Close()强制断开所有连接;Shutdown()只停 accept,等已有请求自然结束——前提是 handler 内部用了ctx.Done()检查
文件描述符继承才是 socket 复用的唯一可靠路径
Linux 下两个进程不能“同时监听同一端口”,SO_REUSEPORT 是内核调度新连接,旧进程 Shutdown 中仍可能收到新连接并返回 connection refused;macOS 甚至不支持 SO_REUSEPORT。真正复用 socket,只能靠 fd 传递。
- 父进程收到
SIGUSR2后,对 listener 做精确类型断言:file, _ := ln.(*net.TCPListener).File()(不是ln.(net.Listener).File()) - 子进程启动时,通过
os.StartProcess的ExtraFiles把file.Fd()传进去(索引从 3 开始,前 3 个是 stdin/stdout/stderr) - 子进程第一行代码必须是:
fd := os.NewFile(3, "")→ln, _ := net.FileListener(fd)→ 然后传给srv.Serve(ln) - 验证是否成功:
lsof -i :8080应只显示一个 PID;若看到两个,说明 fd 没传好或子进程没及时接管
fd 传递和 Shutdown() 的配合点最容易被跳过:handler 里没检查 req.Context().Done(),连接就卡住不结束;子进程恢复 listener 前重定向了 os.Stderr,os.NewFile(3, "") 就会返回 nil。这两处一错,平滑就变成“半滑”。











