【Go Web开发】拦截关闭信号

在深入了解如何拦截信号的具体细节之前,让我们将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

你可能感兴趣的:(【Go Web开发】拦截关闭信号)