go语言没有热重启,本质是旧进程用shutdown()等请求结束、新进程通过fd继承复用监听socket接管新连接,必须协同shutdown、文件描述符传递和sigusr2信号三者。

Go 语言没有“热重启”这回事,所谓优雅重启,本质是旧进程等完请求、新进程复用监听 socket 接管新连接——必须靠 http.Server.Shutdown + 文件描述符传递 + SIGUSR2(或 SIGHUP)信号三者协同,缺一不可。
为什么 srv.Shutdown() 不等于重启
http.Server.Shutdown() 只负责优雅关闭:停止 Accept()、等待活跃请求自然结束或超时。它不启动新进程,不传递监听 socket,也不监听任何信号。
常见错误现象:
- 调了
srv.Shutdown(ctx)就以为重启完成,结果服务彻底退出,没新进程接替 - 直接
os.Exit(0)或发SIGKILL,客户端收到connection reset by peer - 只调
srv.Close(),请求被立即中断,goroutine泄漏、数据库连接未释放
关键区别:Shutdown() 是“收尾”,不是“换人”。你得自己写逻辑拉起新进程,并把老 listener 的 fd 交过去。
如何让新进程复用监听端口而不报 address already in use
核心是文件描述符(fd)继承,不是重新 bind。Linux 下必须显式传递 listener 对应的 fd,Windows 不支持该方案。
实操三步不能漏:
- 父进程调
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}—— 缺少3,子进程就拿不到 listener - 子进程读取环境变量(如
os.Getenv("LISTENER_FD")),用os.NewFile(3, "")恢复*os.File,再用net.FileListener(file)转成可监听的net.Listener
验证方式:lsof -i :8080 应同时看到两个 PID 在监听同一端口;若只看到一个,说明 fd 传递失败或子进程没正确调用 net.FileListener。
Go语言(Golang)1.26.0版本提供 Go 官方 Windows amd64 MSI 安装包下载入口,版本号 1.26.0,可用于旧项目维护、兼容性测试和指定版本开发环境配置。
信号选 SIGUSR2 还是 SIGHUP?别依赖第三方库默认值
现代容器编排(Kubernetes、systemd)默认发 SIGTERM,而 endless 等老库默认监听 SIGHUP,信号收不到就根本不会触发重启。
更关键的是,SIGUSR2 是 POSIX 标准中为“用户自定义重启”保留的信号,语义清晰、冲突少。实操建议:
- 用
signal.Notify(quit, syscall.SIGUSR2)显式监听,不依赖库封装 - 收到信号后,先调
ln.File()拿 fd,再构造子进程参数和Files列表 - 父进程在
srv.Shutdown()返回后才退出,确保所有请求已结束 - 子进程启动后第一件事就是恢复 listener 并启动
srv.Serve(),不能延迟
注意:endless 最后更新是 2021 年,不兼容 Go 1.21+ 的 syscall 变更;graceful 已归档;它们内部仍用过时的 syscall.ForkExec,在 macOS 和新版 Linux 上容易出错。
context.WithTimeout 怎么设才不卡住或误杀
超时时间不是越长越好,而是要匹配业务最长请求耗时。设太短(如 5s)会强杀文件上传类请求;设太长(如 10min)拖慢发布节奏,还可能掩盖 handler 中未响应 ctx.Done() 的 bug。
推荐设为 30–60s,并注意两点:
- 不要写
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second); defer cancel()——defer cancel()会在Shutdown()返回前就触发,导致 ctx 提前取消 - handler 内部必须用传入的
ctx控制所有阻塞操作,比如db.QueryContext(ctx, ...)、http.DefaultClient.Do(req.WithContext(ctx)),否则Shutdown()会等满超时才强制断开
真正容易被忽略的点:新进程启动后,不能立刻调 listener.Accept() —— 要等父进程完成 Shutdown() 并 close listener,否则可能因 socket 处于 TIME_WAIT 或 fd 竞争导致 accept 失败。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










