endless.ListenAndServe 不能与 gin.Engine.Run 混用,必须删除所有 router.Run() 调用;容器中需用 --init 或 tini 保证 SIGUSR2 传递;配置热加载须用 sync.RWMutex 保护并原子替换;endless 仅换进程不 reload 代码,需预部署新二进制。

endless.ListenAndServe 不能和 gin.Engine.Run 混用
直接替换 router.Run() 为 endless.ListenAndServe() 是唯一安全的接入方式。两者底层对 listener 的绑定逻辑冲突:前者调用 http.Server.Serve() 后阻塞,后者在 fork 子进程前必须先完成 socket 复制。若混用,新进程启动时会报 listen tcp :8080: bind: address already in use。
常见错误写法:
router.Run(":8080") // 错误:已占端口
endless.ListenAndServe(":8080", router) // 必然失败
- 必须删掉所有
router.Run或ginEngine.Run调用 - 确保整个服务生命周期只由
endless.ListenAndServe控制 - 若项目含多个 HTTP server(如 metrics 端口),需单独用标准
http.Server启动,避免干扰主监听器
信号权限问题在容器中高频触发
容器默认 init 进程(如 sh)会接管 SIGUSR2,导致 endless 收不到热重启信号。现象是执行 kill -12 <pid> 后无反应,旧进程持续运行,新二进制也未拉起。
验证方法:在容器内执行 ps -o pid,ppid,comm,若主进程 PPID 不是 1,说明 init 未正确传递信号。
- 启动容器时加
--init参数(Docker 默认使用 tini) - 或显式用
tini -- endless.ListenAndServe(...) - Kubernetes 中可在
pod.spec.containers[].command前插入[ "/sbin/tini", "--" ] - 别依赖
alpine:latest自带的 init —— 它不转发SIGUSR2
配置热加载必须绕过 Gin 自身机制
Gin 没有内置配置热加载能力,所谓“Gin 支持热更新”是常见误解。真正要改的是你自己的配置结构体,不是路由或中间件注册逻辑。
典型错误是监听文件变更后直接赋值全局变量:config = newConfig —— 这会造成读写竞争,handler 可能读到半截数据。
- 用
sync.RWMutex包裹配置结构体,读操作走RUnlock(),写操作走Lock() - fsnotify 监听
WriteEvent时,先解析新配置到临时变量,校验通过再原子 swap - 特别注意编辑器行为:VS Code、vim 保存时可能先写
config.yaml~再 rename,需过滤掉以~或.tmp结尾的路径 - HTTP handler 中每次读配置都应取副本(
configCopy := *configPtr),而非指针解引用
endless 不 reload 代码,只换进程
这是最容易被忽略的本质:endless 不是 hot-swap,它不会重新加载 .go 源码或重编译内存中的函数。你必须手动构建新二进制,并用 kill -12 触发切换。
开发阶段若想改代码即生效,请用 air;生产环境上线前,务必确认新二进制已部署到位,且与旧版本 ABI 兼容(尤其涉及 gRPC 接口或共享内存结构时)。
- 新进程启动失败时,旧进程继续服务 —— 这是优点,但也意味着你得主动检查日志里是否有
exec: "xxx": executable file not found - 若新二进制路径写错,或缺失动态链接库(如 musl vs glibc),endless 会静默 fallback 到旧进程,不报错也不提示
- 建议在启动脚本中加入
ls -l /path/to/new-binary && /path/to/new-binary -v预检


















