在深入了解如何拦截信号的具体细节之前,让我们将http.Server相关的代码从main()函数中移出,并放入一个单独的文件中。这将给我们一个更清晰的起点,从这里我们可以建立优雅的关机功能。
创建cmd/api/server.go文件
touch cmd/api/server.go
添加app.serve()方法,对http.Server进行初始化,启动http服务:
File:cmd/api/server.go
package main
...
func (app *application) server() error {
srv := &http.Server{
ErrorLog: log.New(app.logger, "", 0),
Addr: fmt.Sprintf(":%d", app.config.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
//打印服务启动日志
app.logger.Info("starting server", map[string]string{
"addr": srv.Addr,
"env": app.config.env,
})
//启动http服务
return srv.ListenAndServe()
}
完成这些后,我们可以简化main()函数,使用这个新的app.serve()方法,如下所示:
File: cmd/api/main.go
package main
....
func main() {
...
logger := jsonlog.New(os.Stdout, jsonlog.LevelInfo)
db, err := openDB(cfg)
if err != nil {
logger.Fatal(err, nil)
}
defer db.Close()
logger.Info("database connection pool established", nil)
// 添加models字段,引入数据库模型为接口处理程序所用
app := &application{
config: cfg,
logger: logger,
models: data.NewModels(db),
}
// 启动HTTP服务
err = app.server()
if err != nil {
logger.Fatal(err, nil)
}
}
捕获SIGINT和SIGTERM信号
接下来更新我们的应用程序,以便它“捕获”任何SIGINT和SIGTERM信号。如上所述,SIGKILL信号是不能捕获的(并且总是会导致应用程序立即终止),我们将保留SIGQUIT的默认行为(因为如果您想通过键盘快捷键执行不优雅的关闭,会很方便)。
为了捕获这些信号,我们需要运行一个后台goroutine,这个goroutine在应用程序的整个生命周期内都在运行。在这个后台goroutine中,我们可以使用signal.Notify()函数来监听特定的信号,并将它们中继到一个通道以便进一步处理。
打开cmd/api/server.go文件,更新代码如下:
File: cmd/api/server.go
package main
...
func (app *application) server() error {
srv := &http.Server{
ErrorLog: log.New(app.logger, "", 0),
Addr: fmt.Sprintf(":%d", app.config.port),
Handler: app.routes(),
IdleTimeout: time.Minute,
ReadTimeout: 10 * time.Second,
WriteTimeout: 30 * time.Second,
}
//启动一个后台goroutine
go func() {
//创建quit通道来接收信号值
quit := make(chan os.Signal, 1)
//使用signal.Notify()监听SIGINT和SIGTERM信号,并将信号写入quit通道,其他信号忽略。
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
//从quit通道读取信号信息,代码会阻塞在这里直到通道有信号可读
s := <-quit
//打印信号信息到日志中
app.logger.Info("shutting down server", map[string]string{
"signal": s.String(),
})
// 使应用程序正常退出
os.Exit(0)
}()
app.logger.Info("starting server", map[string]string{
"addr": srv.Addr,
"env": app.config.env,
})
return srv.ListenAndServe()
}
目前,新增的代码并没有做太多事情——在拦截信号之后,我们所做的就是记录一条日志,然后退出我们的应用程序。但重要的是,它演示了如何捕获特定信号并在代码中处理这些信号的方法。
需要说明的是,这里创建的quit通道是一个带缓存类型的通道。我们使用带缓存通道的目的是,signal.Notify()函数不需要等待quit通道接收者就可以发送信号到quit通道。如果我们使用不带缓存的通道,信号可能会丢失,因为没有就绪的接收者,信号就无法发送到不带缓存的quit通道中去。通过使用缓冲通道,我们避免了这个问题,并确保我们不会错过一个信号。
下面我们来测试下。
首先,启动服务然后在键盘上按Ctr+C发送SIGINT信号,你将看到"caught signal"日志以及“signal“:“interrupt”:
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T02:48:33Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T02:48:33Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
^C{"level":"INFO","time":"2021-12-19T02:48:35Z","message":"caught signal","properties":{"signal":"interrupt"}}
你可以重启服务并发送SIGTERM信号,日志信息会包含:“signal”:“terminated“:
$ pkill -SIGTERM api
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T02:50:04Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T02:50:04Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
{"level":"INFO","time":"2021-12-19T02:50:06Z","message":"caught signal","properties":{"signal":"terminated"}}
相反,发送SIGKILL或SIGQUIT信号将导致应用程序在没有被捕获的情况下立即退出,所以你不会在日志中看到“caught signal”消息。如果您重新启动应用程序并发出SIGKILL…
pkill -SIGKILL api
应用程序会立即被终止,日志如下所示:
$ go run ./cmd/api
{"level":"INFO","time":"2021-12-19T02:56:11Z","message":"database connection pool established"}
{"level":"INFO","time":"2021-12-19T02:56:11Z","message":"starting server","properties":{"addr":":4000","env":"development"}}
signal: killed