热更新代理的核心是用atomic.Value原子替换http.Handler指针。需监听文件变化、异步构建新handler、确保依赖就绪,避免init状态残留和请求中编译等常见错误。

热更新代理的核心其实是监听文件变化并安全替换 HTTP handler
Go 本身不支持运行时替换函数或类型,所谓“热更新”代理,本质是用一个可变的 http.Handler 指针,在新代码编译加载后,原子地切换它。关键不在“重载代码”,而在“不中断请求”的 handler 替换机制。你不需要 fork 进程、也不需要 RPC 加载插件——那太重了;轻量级方案靠的是 sync/atomic + http.ServeMux 的组合封装。
用 atomic.Value 安全替换 handler 是最简可行路径
直接赋值 handler = newHandler 有竞态风险:正在处理的请求可能看到半新半旧的状态。必须用 atomic.Value,它能保证读写 handler 的原子性,且零内存分配。
- 声明全局变量:
var currentHandler atomic.Value - 初始化时存入默认 handler:
currentHandler.Store(http.HandlerFunc(defaultHandler)) - 在 HTTP 服务中统一读取:
handler := currentHandler.Load().(http.Handler),再调用handler.ServeHTTP(w, r) - 更新时只需:
currentHandler.Store(newHandler)—— 这一行就是热更新的全部动作
注意:newHandler 必须是已构建好的完整 http.Handler(比如重新 http.NewServeMux() 并注册路由),不能只改某个路由函数——因为 http.ServeMux 不支持运行时增删路由的线程安全操作。
监听 main.go 或业务文件变化,触发 rebuild + reload
别自己写 inotify 循环。用现成工具更稳:air 或 reflex 负责检测文件变更、执行 go build、杀旧进程、启新二进制。但注意:它们默认是进程级重启,不是 handler 热更新。
- 若你真要零停机 handler 级热更新,就得自己写一个轻量 watcher,例如用
fsnotify监听./handlers/*.go - 检测到变化后,调用
go run build-handler.go(该脚本编译 handler 包为独立模块,或生成闭包函数) - 再通过
currentHandler.Store(...)切换——这要求你的 handler 构建逻辑是纯函数式、无副作用、可重复调用 - 常见坑:
init()函数只执行一次,如果新 handler 依赖 init 初始化的全局状态(如数据库连接池),热更新后这些状态不会刷新,会出问题
不要在 handler 内部做耗时重载,否则阻塞所有请求
有人把 “编译 + 加载 + 解析” 全塞进 ServeHTTP 方法里,这是灾难性的。每次请求都去 exec.Command("go", "build")?CPU 瞬间拉满,还可能并发冲突。
- 重载动作必须异步:文件变化 → 启动 goroutine 构建新 handler → 构建成功后
Store() - 构建失败时,保留旧 handler,记录错误日志,绝不 panic 或降级为空 handler
- 加个简单锁(
sync.Mutex)保护构建过程,避免同一时间多次构建冲突 - 可选:加个
healthz接口返回当前 handler 的 hash 或编译时间,方便验证是否真的更新了
真正难的不是切换 handler,而是确保新 handler 的依赖(配置、连接、缓存)在切换瞬间就绪且一致——这部分没法靠语言特性兜底,得你来设计生命周期。











