需求背景
在开发 Go Web 应用时,应用的代码更新、配置的修改或者资源文件的变化都可能影响到应用的运行,在传统的方式下每次修改都需要重启应用,而这种方式会导致用户的访问受影响,因此我们需要一种方式能够在不影响用户访问的情况下热重启应用。
实现思路
由于 Go 没有像其他语言那样提供官方的热重启功能,因此我们需要通过以下方式实现:
- 当程序启动时,启动一个新的 goroutine 监控配置文件或程序文件,如果文件有变化,则发送指令给主 goroutine。
- 主 goroutine 接收到指令之后,创建一个新的 goroutine,在该 goroutine 中执行新的代码,然后设置一个新的监听端口启动 ListenAndServe,绑定至一个独立的端口,由新的 goroutine 提供新的服务。
- 新的 goroutine 启动之后,主 goroutine 关闭旧的服务,终止监听并退出 goroutine,然后将新的服务切换至原来的端口,这样用户就可以完全无感知地继续访问应用。
热重启示例
下面是一个简单的示例,演示如何通过修改监听端口实现热重启。
首先,我们需要编写一个简单的 web 应用:
package main
import (
"fmt"
"net/http"
)
func helloWorld(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello, World!")
}
func main() {
http.HandleFunc("/", helloWorld)
fmt.Println("[INFO] Server is listening on :8080 ...")
http.ListenAndServe(":8080", nil)
}
接下来,我们需要编写一个新的 main() 函数,该函数负责启动一个 goroutine 监听代码变化,并进行热重启操作:
package main
import (
"fmt"
"net/http"
"os"
"syscall"
"time"
)
// 检查文件是否有更改
func watchFileChange(path string) <-chan bool {
c := make(chan bool)
go func() {
var lastModTime time.Time
for {
fileInfo, err := os.Stat(path)
if err == nil {
modTime := fileInfo.ModTime()
if !lastModTime.IsZero() && modTime.After(lastModTime) {
fmt.Println("[INFO] Detected code change, restarting ...")
c <- true
}
lastModTime = modTime
}
time.Sleep(time.Second)
}
}()
return c
}
func main() {
serverAddr := ":8080"
handler := http.NewServeMux()
handler.HandleFunc("/", func(writer http.ResponseWriter, request *http.Request) {
fmt.Fprintln(writer, "Hello World!")
})
server := http.Server{
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Addr: serverAddr,
Handler: handler,
}
fmt.Println("[INFO] Starting server on " + serverAddr)
// 启动新的 goroutine 监听代码变化
fileChanged := watchFileChange("main.go")
// 启动 http 服务
go func() {
server.ListenAndServe()
}()
// 不断监听文件变化
for {
if <-fileChanged {
fmt.Println("[INFO] Restarting server ...")
// 获取进程 PID
pid := syscall.Getpid()
// 创建新的子进程
procAttr := syscall.ProcAttr{
Dir: ".",
Env: os.Environ(),
Files: []uintptr{os.Stdin.Fd(), os.Stdout.Fd(), os.Stderr.Fd()},
Sys: &syscall.SysProcAttr{},
}
newProc, err := syscall.ForkExec(os.Args[0], os.Args, &procAttr)
if err != nil {
fmt.Printf("[ERROR] Failed to fork: %v\n", err)
continue
}
fmt.Printf("[INFO] New PID: %d\n", newProc)
// 等待子进程退出
syscall.Wait4(newProc, nil, 0, nil)
// 关闭旧的服务
server.Close()
// 启动新的服务
fmt.Println("[INFO] Starting new server ...")
server = http.Server{
Handler: handler,
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
Addr: serverAddr,
}
go func() {
server.ListenAndServe()
}()
}
}
}
在上述代码中,我们首先编写了一个 watchFileChange() 函数,该函数会循环检查指定的文件是否有变化。如果文件有变化,则向一个无缓冲的 chan 发送 true 消息。我们启动一个新的 goroutine 执行该函数,并返回所创建的 chan。
在 main() 函数中,我们首先启动一个 http 服务,然后启动一个新的 goroutine,该 goroutine 通过读取创建的 chan,实现监听文件变化操作。当该 goroutine 接收到指令之后,首先获取当前进程的 PID,并通过 syscall.ForkExec() 函数创建一个新的子进程,该子进程是新的服务进程。
接下来,原进程会关闭旧的服务,根据新的热重启方式,新服务会监听一个新的端口,因此需要重新设置 serverAddr。重新设置 serverAddr 之后,我们可以启动新的服务,然后返回等待文件变化。整个过程类似于进程的 daemon 化操作。
通过以上实现,我们可以在不影响用户访问的情况下,实现 Go 热重启操作。
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:Go 实现热重启的详细介绍 - Python技术站