如何优雅的关闭GoWeb服务器

发布于:2025-02-11 ⋅ 阅读:(107) ⋅ 点赞:(0)

以下内容均为Let’s Go Further内容节选以及作者本人理解。
这里创建了一个后台进程用于捕获关闭信号,在后台进程中,主要内容为:

  1. 创建一个缓冲通道 quit
  2. 使用signal.Notify函数监听并捕获关机信号SIGINT,SIGTERM,在捕获关机信号后,缓冲通道取值操作才会执行,取消阻塞。
  3. 随后使用shutdown函数关闭服务器。工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
  4. 如果关闭连接时发生错误,则将错误存储进入通道,主程序取出错误,取消阻塞。返回错误。
  5. 如果正常关闭连接,app.wg.Wait()等待全部后台进程全部运行结束后放行,随后向错误通道存储空值,放行主程序。此时连接和后台进程全部关闭,打印日志,结束。
package main

import (
	"context"
	"errors"
	"fmt"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func (app *application) serve() error {
	srv := http.Server{
		Addr:         fmt.Sprintf(":%d", app.config.port),
		Handler:      app.routes(),
		IdleTimeout:  time.Minute,
		ReadTimeout:  10 * time.Second,
		WriteTimeout: 30 * time.Second,
		//服务器可以编写自己的日志消息,实现Write方法变成io.Write接口可以将自定义日志传递进去
		//ErrorLog: log.New(logger, "", 0),
	}

	shutdownErr := make(chan error)
	//开启一个后台进程 捕获关闭信号,关闭所有连接和后台进程,实现优雅的关机
	go func() {
		//创建一个通道保存os.Signal值
		/*
			在这里使用缓冲通道,如果使用非缓冲通道,quit 通道 在信号发送的时刻没有准备去接收,会错过信号
		*/
		quit := make(chan os.Signal, 1)
		//使用signal.Notify监听SIGINT and SIGTERM信号并将他们传入quit通道。其他信号不会被捕获并保留他们默认的行为。
		signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)

		//从通道中读取值,这个操作将被阻塞直到通道接收一个值
		s := <-quit
		app.logger.PrintInfo("shutting down server", map[string]string{
			"signal": s.String(),
		})
		//创建一个5秒的上下文
		ctx, cancelFunc := context.WithTimeout(context.Background(), 5*time.Second)
		defer cancelFunc()
		//服务调用shutdown,如果成功关闭将返回nil,或者返回错误(当关闭一个监听器时遇到问题,或者在到达截止日期时没有完成)
		/*
			工作原理是首先关闭所有打开的侦听器,然后关闭所有空闲连接,然后无限期地等待连接返回空闲状态,然后关闭。
		*/

		/*
			1. 关闭连接时没有返回错误,说明连接全部关闭,但是还有后台进程,告诉正在执行关闭后台进程,需要等待wg归零。
			2. 关闭连接出现错误。将err存入通道,放行主程序
		*/

		//shutdown返回的是一个error类型的变量
		err := srv.Shutdown(ctx)
		//通道中存入错误,主程序取消阻塞,继续执行
		if err != nil {
			shutdownErr <- err
		}
		//正在完成后台进程
		app.logger.PrintInfo("completing background tasks", map[string]string{
			"addr": srv.Addr,
		})
		app.wg.Wait()
		//后台进程全部关闭后,放行主程序
		shutdownErr <- nil
	}()

	app.logger.PrintInfo("starting server", map[string]string{
		"addr": srv.Addr,
		"env":  app.config.env,
	})
	//调用shutdown会造成ListenAndServer立刻返回一个http.ErrServerClosed错误。如果是这个错误,表明我们的程序优雅的关闭了
	err := srv.ListenAndServe()
	if !errors.Is(err, http.ErrServerClosed) {
		return err
	}
	
	//阻塞  关闭后才能继续执行
	err = <-shutdownErr
	if err != nil {
		return err
	}
	app.logger.PrintInfo("stopped server", map[string]string{
		"addr": srv.Addr,
	})
	return nil
}

关于使用后台进程运行其他任务时,使用函数background来启动后台进程

func (app *application) background(fn func()) {
	//开启一个后台进程
	app.wg.Add(1)
	go func() {
		defer app.wg.Done()
		//使用延迟函数捕获可能出现的panic
		defer func() {
			if err := recover(); err != nil {
				app.logger.PrintError(fmt.Errorf("%s", err), nil)
			}
		}()
		fn()
	}()
}

网站公告

今日签到

点亮在社区的每一天
去签到