有些命令行工具或桌面小工具发布以后,希望用户不用重新下载压缩包,也能在程序内部完成更新。Go 程序做自更新时,常见流程是:
- 请求一个
latest.json,获取最新版本和各平台下载地址。 - 根据当前系统和 CPU 架构选择对应安装包。
- 下载新的二进制文件,同时显示下载进度。
- 计算下载文件的 SHA256,确认文件没有损坏或被替换。
- 用新文件替换当前正在运行的程序。
下面用 github.com/inconshreveable/go-update 做一个比较完整的示例。
安装依赖
先创建一个普通 Go 项目:
1 | mkdir self-update-demo |
安装自更新依赖:
1 | go get github.com/inconshreveable/go-update |
这个库负责把下载好的新程序替换到当前可执行文件的位置,并在失败时尽量回滚。
latest.json 格式
服务端可以放一个 latest.json,里面保存当前最新版本和不同平台的下载地址:
1 | { |
平台名使用 runtime.GOOS + "-" + runtime.GOARCH 拼出来。常见值有:
darwin-amd64darwin-arm64linux-amd64windows-amd64
完整代码
创建 main.go:
1 | package main |
把 latestURL 改成自己的更新地址后,就可以在程序里调用 ApplyUpdate。
代码说明
ProgressReader 包装了原始的 io.Reader,每次读取时累加已经下载的字节数。如果响应里有 Content-Length,就可以计算百分比:
1 | percent := float64(p.Downloaded) / float64(p.Total) * 100 |
这里没有把进度输出写死在 Read 里面,而是通过 OnProgress 回调传出去。这样以后不管是命令行进度条、GUI 进度条,还是日志输出,都可以复用同一个下载逻辑。
fetchLatestInfo 做三件事:
- 下载
latest.json - 解析 JSON
- 根据当前平台取出对应的下载地址和 SHA256
当前平台通过下面的函数得到:
1 | func currentPlatform() string { |
如果当前是 macOS Apple Silicon,结果就是 darwin-arm64;如果是 64 位 Linux,通常是 linux-amd64。
downloadAndVerify 会把下载内容同时写入临时文件和 SHA256 计算器:
1 | io.Copy(io.MultiWriter(tmpFile, hasher), reader) |
这样不用先完整下载文件再读一遍计算哈希。下载完成后,如果实际 SHA256 和 latest.json 里的值不一致,就删除临时文件并返回错误。
最后 ApplyUpdate 会调用:
1 | update.Apply(updateFile, update.Options{}) |
这个操作会尝试用新文件替换当前程序。如果替换过程中失败,可以通过 update.RollbackError(err) 获取回滚阶段的错误。
生成 SHA256
发布新版本时,需要先编译不同平台的程序,再计算每个文件的 SHA256。
Linux 和 macOS 可以使用:
1 | shasum -a 256 myapp-linux-amd64 |
Windows 可以在 PowerShell 中使用:
1 | Get-FileHash .\myapp-windows-amd64.exe -Algorithm SHA256 |
把得到的哈希值填到 latest.json 里即可。
打包不同平台
Go 可以交叉编译不同平台的二进制:
1 | GOOS=linux GOARCH=amd64 go build -o myapp-linux-amd64 |
如果项目依赖 CGO,交叉编译会复杂一些,需要准备对应平台的 C 编译环境。纯 Go 项目通常会简单很多。
常见注意事项
自更新最好使用 HTTPS 地址,避免下载内容在传输过程中被篡改。SHA256 可以发现文件不一致,但不能证明 latest.json 本身可信。如果安全要求更高,可以再加签名校验。
程序需要有权限替换自己。如果可执行文件放在系统目录,例如 /usr/local/bin 或 C:\Program Files,普通用户可能没有写权限,需要管理员权限或安装器配合。
Windows 对正在运行的可执行文件锁定更严格,自替换比 Linux 和 macOS 更容易遇到限制。实际发布前一定要在目标平台上测试一次。
更新完成后,当前进程里运行的仍然是旧代码。通常做法是提示用户重启程序,或者由外部启动器负责重新拉起新版本。
latest.json 里的 version 字段在上面的示例中没有参与判断。如果不希望每次都下载,可以在本地保存当前版本号,先比较版本,再决定是否调用下载逻辑。
小结
这个方案的核心是把更新拆成几个清楚的步骤:获取元信息、选择平台、下载文件、校验哈希、替换程序。只要更新源可靠、哈希值正确,并且目标路径有写入权限,就可以给 Go 命令行工具或桌面小工具加上一个简单可控的自更新能力。