Go如何热更新,其实一直是一个问题。现在比较流行的方式,基本都来自 Graceful Restart In Golang 这篇由 Grisha Trubetskoy 所提出的方案。所以我也依据在佬所提出的方案,实现个简易的热更功能。
一、基本思想
其实方法很简单,就是项目在收到热更请示后,重新编译代码并以启动新编译好的可执行文件。但要注意,启动新的可执行文件时要告知它要关闭父进程。所以思想并不难。
二、基本实现
首先,我们新建一个名为 HotUpdate 的Go项目。我们先定义好,如果项以正常模式启动,则使用 ./HotUpdate
如果使用热更新模式启动,则使用 ./HotUpdate -update
,所以为了实现这一点,我们需要要写定义好启动参数,代码如下。
var isUpdate bool
func init() {
flag.BoolVar(&isUpdate, "update", false, "if you need update this project, set this filed true")
flag.Parse()
}
上段代码定义了 isUpdate 这个 bool 变量用于保存当前是否以 update 方式启动。
然后我们增加一个简单的 http 服务,当收到 /hello
请求时回一个字符串,以模拟普通请求,当收到/update
请求时开始执行热更新。
模拟服务的代码如下:
http.HandleFunc("/hello", hello)
http.HandleFunc("/update", update)
err := http.ListenAndServe(":8080", nil)
接下来就是增加热更新逻辑了,写在 hotUpdate 函数里。逻辑就是先使用 go build
重新编译项目,待项目编译完成后,以热更的模式启动新编译好的可执行文件。
代码如下:
func hotUpdate() {
cmd := exec.Command("go", "build")
err := cmd.Run()
if err != nil {
fmt.Println(err)
}
fmt.Println("build end")
path := os.Args[0]
cmd = exec.Command(path, "-update")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
fmt.Println("child server running")
}
然后我们还需要写一段逻辑,如果是以热更模式启动的服务,则要先杀掉父进程,然后才开始监听端口,不然报会端口已被占用。
代码如下:
func main() {
fmt.Println("Running ...")
if isUpdate {
fmt.Println("hot update, kill parent", syscall.Getppid())
syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
time.Sleep(100 * time.Millisecond) // 等待,确保父进程结束
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/update", update)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}
至此,完整代码如下:
package main
import (
"flag"
"fmt"
"net/http"
"os"
"os/exec"
"syscall"
"time"
)
var isUpdate bool
func init() {
flag.BoolVar(&isUpdate, "update", false, "if you need update this project, set this filed true")
flag.Parse()
}
func hello(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("hello\n"))
}
func update(w http.ResponseWriter, r *http.Request) {
_, _ = w.Write([]byte("start update\n"))
go hotUpdate()
}
func hotUpdate() {
cmd := exec.Command("go", "build")
err := cmd.Run()
if err != nil {
fmt.Println(err)
}
fmt.Println("build end")
path := os.Args[0]
cmd = exec.Command(path, "-update")
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
_ = cmd.Run()
fmt.Println("child server running")
}
func main() {
fmt.Println("Running ...")
if isUpdate {
fmt.Println("hot update, kill parent", syscall.Getppid())
syscall.Kill(syscall.Getppid(), syscall.SIGTERM)
time.Sleep(100 * time.Millisecond)
}
http.HandleFunc("/hello", hello)
http.HandleFunc("/update", update)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Println(err)
}
}
然后编译运行,注意该代码暂时不能运行在 windows,因为 window 环境下没有 syscall.Kill(pid, syscall.SIGTERM)
函数,所以如果要运行在 windows 上,则还需要处理一下。
同时注意不要使用浏览器直接访问 localhost:8080/update,因为热更本质上会重启,浏览器会在服务重启后重新请示,会导致服务一起热更。
所以要使用 curl localhost:8080/update
方式请求。
接下来测试代码,启动项目后使用 curl 请示。
curl loaclhost:8080/hello
hello
// 修改代码中 hello 函数,将反回的字符串改为 "hello, world\n"
curl localhost:8080/update
start update
curl localhost:8080/hello
hello, world
至此热更成功。