开发阶段用 fresh 实现自动重启最轻量,生产环境须用优雅关机+外部拉起;fresh 监听 .go 文件变化并重启进程,而 syscall.Exec 热替换更底层但不支持 Windows;生产必须用 SIGTERM 触发 srv.Shutdown() 并手动处理 WebSocket。

直接用 r.Run() 启动 Gin 服务无法实现自动重启,因为它封装了阻塞式 http.ListenAndServe(),既不暴露 *http.Server 实例,也无法监听文件变更或响应信号。开发阶段要自动重启,必须绕过 r.Run(),改用进程级热重载工具或手写 watcher;生产环境则需区分「开发自动重启」和「生产优雅重启」——二者目标、机制、风险完全不同。
开发时用 fresh 实现代码保存即重启
这是最轻量、无侵入、适合本地调试的方式,不修改业务代码,也不依赖信号或 fork。
-
fresh是一个 Go 源码监控 + 自动go run的小工具,只监听.go文件变化,触发后杀掉旧进程、启动新实例 - 安装:
go install github.com/pilu/fresh@latest(注意:不是go get,新版 Go 推荐用go install) - 项目根目录下执行:
fresh,它会自动找main.go并运行;若入口文件名不同,加-c指定配置文件或用-cmd指定命令 - 默认忽略
vendor/、testdata/和以.或_开头的文件,如需监控config.yaml,得在fresh.conf里显式添加watch = ["config.yaml"] - ⚠️ 坑点:它用
os.StartProcess启动新进程,但不会等待旧进程完全退出再启动新进程,极端情况下可能短暂端口冲突(address already in use);可通过加-delay 500(单位 ms)缓解
用 fsnotify + syscall.Exec 手写二进制热替换
比 fresh 更底层、更可控,适用于需要复用同一进程 PID(比如 systemd 管理)、或想避免反复 go run 编译开销的场景。
- 核心是监听当前可执行文件(
osext.Executable())是否被go build -o覆盖,一旦检测到变更,就调用syscall.Exec(file, os.Args, os.Environ())替换自身 -
syscall.Exec不是 fork + exec,而是直接用新镜像覆盖当前进程内存空间,原 goroutine 全部终止,所以不用关 DB 连接、不用等 handler 返回——这恰恰是开发期可以接受的“不优雅”,也是它快的原因 - 必须确保构建命令输出路径与监听路径一致,例如:
go build -o ./server ./cmd/server,且setupWatcher()中w.Add(file)的file就是这个./server - ⚠️ 坑点:Windows 不支持
syscall.Exec;macOS/Linux 下若用 IDE 自带构建(如 VS Code 的 Ctrl+F5),可能因工作目录或输出路径不一致导致监听失效
生产环境别用自动重启,改用优雅关机 + 外部拉起
自动重启是开发便利性功能,生产环境必须杜绝「无感知覆盖二进制」行为。真正的平滑发布,是旧进程收到 SIGTERM 后完成正在处理的请求(含 WebSocket 主动断连),再由 systemd / supervisor / k8s 等拉起新版本。
- 关键动作不是“重启”,而是:1)主 goroutine 阻塞等待
syscall.SIGTERM或syscall.SIGINT;2)收到后调srv.Shutdown(ctx);3)defer关闭 DB/Redis/Kafka 客户端;4)最后才退出进程 - 不要用
fvbock/endless:它已归档,且不兼容 Go 1.20+ 的net.Listener接口变更;现代做法是手写*http.Server生命周期管理,或用graceful等轻量库 - 若需零停机更新,必须配合外部机制:比如用
systemd的Restart=on-failure+ExecReload=/bin/kill -s SIGTERM $MAINPID,或 k8s 的 rolling update - ⚠️ 坑点:WebSocket 连接不会被
srv.Shutdown()自动关闭,必须在关机前遍历sync.Map活跃连接池,发websocket.CloseMessage并设标志禁止新Upgrade
真正容易被忽略的是信号语义混淆:开发期用 SIGUSR2 触发热重载(Linux/macOS),而生产关机必须用 SIGTERM;混用会导致 systemd 认为进程异常退出,反复拉起。另外,所有 long-running handler(比如带 time.Sleep(5 * time.Second) 的接口)都得主动检查 c.Request.Context().Done(),否则 Shutdown() 会超时强制终止,客户端收不到完整响应。


















