go-selfupdate是最成熟可落地的cli自更新方案,但需手动配置服务端清单和二进制分发,且不支持热重启;手写http更新易遇windows原子替换失败、无校验致静默损坏、信号处理缺失、无增量更新等坑。

go-selfupdate 是目前最成熟、可直接落地的 CLI 自更新方案,但必须手动配置服务端清单和二进制分发,且不支持热重启(需进程替换)。
为什么不用自己手写 HTTP 下载 + 版本比对
自己用 net/http 请求版本文件、比对 CurrentVersion、调用 os.Rename 替换二进制,看似简单,实际踩坑密集:
- Windows 上无法原子替换正在运行的 exe 文件,会报
Access is denied - 没做 SHA256 校验时,网络中断或 CDN 缓存污染会导致静默损坏
- 没处理信号(如
SIGINT)就中断下载,临时文件残留、权限错乱很常见 - 增量更新完全没做,10MB 的 CLI 每次都重下全量,用户流失率直线上升
go-selfupdate 初始化必须填对的 4 个 URL 字段
ApiURL、BinURL、DiffURL 看似重复,但作用完全不同:
-
ApiURL返回 JSON 清单,含Version、BinSHA256、DiffSHA256、PublishedAt—— 必须是带版本路径的 endpoint,例如"https://updates.example.com/v1/myapp/manifest.json" -
BinURL是完整二进制下载地址,模板为"https://updates.example.com/v1/myapp/{{.Version}}/myapp-{{.OS}}-{{.Arch}}",selfupdate会自动补全变量 -
DiffURL是 bsdiff 补丁地址,格式与BinURL一致,但后缀为.diff;若该 URL 返回 404,它会自动 fallback 到完整下载 - 三者域名可以不同,但协议必须一致(全 HTTPS 或全 HTTP),否则 iOS/macOS Gatekeeper 可能拦截
发布新版本时 go-selfupdate 命令的两个关键约束
运行 go-selfupdate myapp 1.2 生成更新包前,必须确保:
- 当前目录下存在已编译好的
myapp二进制(对应旧版),且myapp --version输出与代码中version变量严格一致(含v前缀) - 目标版本号(如
1.2)必须是语义化版本(v1.2.0或1.2.0),不能是main-20260407这类 commit-hash 风格,否则selfupdate.Updater解析失败 - 若要支持 macOS Apple Silicon(arm64),需提前交叉编译出
myapp-darwin-arm64并放在同一目录,工具会自动识别并打包
BackgroundRun() 启动后最容易被忽略的三件事
updater.BackgroundRun() 是非阻塞的,但它背后依赖几个隐性前提:
- 它默认每 24 小时检查一次,不是启动即查 —— 若想首次启动就检查,得显式调用
updater.Update() - 它不会自动重启进程:更新成功后,
updater.Info.Version是新版本号,但当前进程仍是旧二进制,需自行调用os.Exec替换(Linux/macOS)或syscall.StartProcess(Windows) - 临时目录
Dir(默认"update/")必须有写权限,且不能在 tmpfs 或容器/tmp这类易失路径 —— 否则重启后补丁丢失,下次又得下全量
真正麻烦的从来不是“怎么触发更新”,而是“更新后如何安全交棒给新进程”——尤其当 CLI 持有 open file descriptor、绑定本地 socket、或正在执行子命令时,粗暴 exec 容易导致资源泄漏或状态不一致。这一步没有银弹,必须按你的 CLI 生命周期定制。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











