Golang二进制文件的OTA更新实现
Golang二进制文件的OTA更新实现 有没有办法向使用Go构建的二进制应用程序推送OTA(空中下载)更新?具体如何操作?
我考虑过以下逻辑,但不确定是否可行,特别是第5、6、7点,因为应用程序届时已经关闭了?!
// 1. Check current binary version
// 2. Read latest version from repository
// 3. Download the new version (if any)
// 4. Shut done current instant of the app
// 5. Delete the current app binary from the device
// 6. Move the newly downloaded version to the original path of the app
// 7. Launch an instant of the new version
更多关于Golang二进制文件的OTA更新实现的实战教程也可以访问 https://www.itying.com/category-94-b0.html
你需要手动创建一个OTA功能吗?
是的
MinIO分布式文件系统项目具备二进制升级功能,并且是使用Go语言编写的。
luk4z7:
我只了解嵌入式项目中的OTA,比如乐鑫。
怎么实现的?我看了你分享的链接,但没找到。
那么,你是否开发任何嵌入式软件?因为有很多工具可以用于部署到服务器。

A curated list of awesome Go frameworks, libraries and software - Awesome Go
一个精心整理的优秀 #Golang 框架、库和软件列表
或者你需要手动创建一个 OTA 功能吗?
ESP-IDF OTA 文档 FreeRTOS OTA 开发人员指南
但这些链接与 Go 语言没有任何关系。
我明白了。据我所知,目前没有用Go语言实现这个功能的项目。我只了解嵌入式项目中的OTA(空中下载技术),比如Espressif、FreeRTOS和IOT Core等等,这涉及到客户端和服务器端。现在如果你需要创建这个功能,我认为服务器端更常见,但客户端端则取决于设备类型,例如ESP32,以及你通过何种方式(如蓝牙、MQTT等)来发送OTA字节数据。
我之前实现过这个功能,但当时仅针对单一操作系统(CentOS)和一个持续运行的服务。我是通过以下步骤完成的:
- 在应用程序中启动一个goroutine,按照预定义的时间间隔检查新版本(具体间隔根据你的需求而定)。
- 如果存在新版本,则获取新的二进制文件和静态内容(我有构建二进制文件的工具;如果没有,你可以获取源代码并构建新的二进制文件。显然,如果你想构建二进制文件,就需要在目标机器上安装Go作为依赖)。
- 此时,通过
exec.Command执行一个更新脚本,该脚本:- 运行数据库更新(不确定你是否有数据层,但大多数应用程序都有)。
- 确保新的应用程序二进制文件具有
+x可执行权限。 - 将新二进制文件复制覆盖当前正在运行的二进制文件(CentOS允许这样做)。
- 运行
systemctl restart $myService来重启服务并使用新的二进制文件。
总而言之:实现此功能的一个简单方法是随应用程序附带一个更新器。它可以是一个脚本,也可以是它自己的二进制文件。根据你构建的应用程序类型(它是一个持续运行的服务吗?还是一个用户启动的GUI应用程序?等等),你需要让更新器以不同的方式工作。
对于Go二进制文件的OTA更新,你的思路基本正确,但需要处理自我替换的问题。以下是实现方案:
package main
import (
"fmt"
"io"
"net/http"
"os"
"os/exec"
"path/filepath"
"runtime"
)
// 主程序更新逻辑
func updateBinary() error {
// 1. 检查当前版本
currentVersion := getCurrentVersion()
// 2. 从仓库获取最新版本
latestVersion, err := fetchLatestVersion()
if err != nil {
return err
}
if currentVersion >= latestVersion {
return nil // 无需更新
}
// 3. 下载新版本
newBinaryPath, err := downloadNewVersion(latestVersion)
if err != nil {
return err
}
// 4. 启动更新器进程进行替换
return launchUpdater(newBinaryPath)
}
// 下载新版本
func downloadNewVersion(version string) (string, error) {
url := fmt.Sprintf("https://example.com/app/v%s/app-%s-%s",
version, runtime.GOOS, runtime.GOARCH)
resp, err := http.Get(url)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 创建临时文件
tmpFile, err := os.CreateTemp("", "update-*.tmp")
if err != nil {
return "", err
}
defer tmpFile.Close()
// 写入临时文件
_, err = io.Copy(tmpFile, resp.Body)
if err != nil {
return "", err
}
// 设置可执行权限
if runtime.GOOS != "windows" {
os.Chmod(tmpFile.Name(), 0755)
}
return tmpFile.Name(), nil
}
// 启动更新器进程
func launchUpdater(newBinaryPath string) error {
// 获取当前可执行文件路径
exePath, err := os.Executable()
if err != nil {
return err
}
// 创建更新器脚本/程序
updaterScript := createUpdaterScript(exePath, newBinaryPath)
// 启动更新器(独立进程)
cmd := exec.Command(updaterScript)
if err := cmd.Start(); err != nil {
return err
}
// 主程序退出
os.Exit(0)
return nil
}
// 创建更新器脚本
func createUpdaterScript(currentPath, newPath string) string {
var script string
if runtime.GOOS == "windows" {
script = fmt.Sprintf(`@echo off
timeout /t 2 /nobreak >nul
del "%s"
move "%s" "%s"
start "" "%s"`,
currentPath, newPath, currentPath, currentPath)
scriptPath := filepath.Join(os.TempDir(), "updater.bat")
os.WriteFile(scriptPath, []byte(script), 0644)
return scriptPath
} else {
script = fmt.Sprintf(`#!/bin/bash
sleep 2
rm -f "%s"
mv "%s" "%s"
chmod +x "%s"
"%s" &`,
currentPath, newPath, currentPath, currentPath, currentPath)
scriptPath := filepath.Join(os.TempDir(), "updater.sh")
os.WriteFile(scriptPath, []byte(script), 0755)
return scriptPath
}
}
// 版本管理函数
func getCurrentVersion() string {
// 从嵌入信息或配置文件读取
return "1.0.0"
}
func fetchLatestVersion() (string, error) {
// 从API或版本文件获取
resp, err := http.Get("https://example.com/version.txt")
if err != nil {
return "", err
}
defer resp.Body.Close()
version, _ := io.ReadAll(resp.Body)
return string(version), nil
}
对于关键的第5、6、7点,解决方案是:
- 使用独立更新器进程:主程序启动一个独立的更新器进程(脚本或Go程序),然后退出
- 更新器等待主程序退出:更新器等待几秒确保主程序完全退出
- 执行替换操作:更新器删除旧文件,移动新文件到原位置
- 启动新版本:更新器启动新版本程序
更健壮的实现可以使用专门的更新器二进制文件:
// updater.go - 独立的更新器程序
package main
import (
"fmt"
"os"
"path/filepath"
"time"
)
func main() {
if len(os.Args) < 3 {
fmt.Println("Usage: updater <current> <new>")
os.Exit(1)
}
currentPath := os.Args[1]
newPath := os.Args[2]
// 等待主程序退出
time.Sleep(2 * time.Second)
// 删除当前文件
os.Remove(currentPath)
// 移动新文件
err := os.Rename(newPath, currentPath)
if err != nil {
// 如果跨卷移动失败,使用复制
copyFile(newPath, currentPath)
os.Remove(newPath)
}
// 设置权限
if runtime.GOOS != "windows" {
os.Chmod(currentPath, 0755)
}
// 启动新程序
cmd := exec.Command(currentPath)
cmd.Start()
}
func copyFile(src, dst string) error {
in, err := os.Open(src)
if err != nil {
return err
}
defer in.Close()
out, err := os.Create(dst)
if err != nil {
return err
}
defer out.Close()
_, err = io.Copy(out, in)
return err
}
主程序调用方式:
func launchUpdater(newBinaryPath string) error {
exePath, _ := os.Executable()
// 编译或使用预编译的更新器
updaterPath := "/path/to/updater"
cmd := exec.Command(updaterPath, exePath, newBinaryPath)
if err := cmd.Start(); err != nil {
return err
}
os.Exit(0)
return nil
}
这种方法确保了:
- 主程序完全退出后才进行文件操作
- 使用独立进程避免文件锁定问题
- 支持跨平台操作(Windows/Linux/macOS)
- 提供回滚机制(可保留旧版本备份)

