Jager / go程序优雅退出

Created Tue, 03 Aug 2021 00:00:00 +0000 Modified Tue, 07 May 2024 14:11:16 +0800

服务端程序是持续不断运行的,在停服时就需要等待各种服务关闭后再退出程序, 本文将介绍go程序优雅退出目前比较推荐的一种实现方式

原理

通过go1.16+的NotifyContext方法和errgroup包实现服务的优雅停止

代码

package main

import (
	"context"
	"errors"
	"fmt"
	"github.com/gin-gonic/gin"
	"golang.org/x/sync/errgroup"
	"log"
	"net/http"
	"os/signal"
	"syscall"
	"time"
)

type ginHttp struct {
	server *http.Server
}

func NewGinHttpServer(listenIp string, listenPort int, f func(engine *gin.Engine)) *ginHttp {
	engine := gin.Default()
	f(engine)
	s := &http.Server{
		Addr:    fmt.Sprintf("%s:%d", listenIp, listenPort),
		Handler: engine,
	}

	return &ginHttp{
		server: s,
	}
}

func (g *ginHttp) Serve() <-chan error {
	// chan长度大于等于1, 不然errch不存在读取取,将永远阻塞在这里,造成goroutine泄露
	errch := make(chan error, 1)
	go func() {
		err := g.server.ListenAndServe()
		if err != nil {
			errch <- err
			log.Printf("服务停止:%v", err)
		}
		close(errch)
	}()

	return errch
}

func (g *ginHttp) Stop() error {
	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
	defer cancel()
	return g.server.Shutdown(ctx)
}

func main() {
	// 创建通过监听信号syscall.SIGINT, syscall.SIGEMT来停止的context
	ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGEMT)
	defer cancel()

	// 通过errgroup来运行多个服务
	g, ctx := errgroup.WithContext(ctx)

	// 服务一
	g.Go(func() error {
		tr := time.NewTimer(time.Second * 30)
		select {
		// 监听context是否被取消, 取消则终止服务
		case <-ctx.Done():
			/*
				停止服务操作
				stop()
			*/
			log.Println("收到context取消信号,停止服务一")
			return ctx.Err()

		// 模拟服务
		case <-tr.C:
			// 模拟30秒后服务出错返回, errgroup会对errgroup.WithContext返回的context进行取消
			log.Println("服务一出错: xxx")
			return errors.New("xxx")
		}
	})

	// 服务二: 基于gin框架的web服务
	g.Go(func() error {
		ginSvr := NewGinHttpServer("0.0.0.0", 8080, func(engine *gin.Engine) {
			engine.GET("/hello", func(c *gin.Context) {
				c.String(http.StatusOK, "Hello World!")
			})
		})
		select {
		// 监听context是否被取消, 取消则终止服务
		case <-ctx.Done():
			//停止服务操作
			log.Println("收到context取消信号,停止服务二")
			err := ginSvr.Stop()
			if err != nil {
				log.Printf("停止服务二出错: %v", err)
			} else {
				log.Println("成功停止服务二")
			}
			return ctx.Err()

		// http服务
		case err := <-ginSvr.Serve():
			// 如果服务完成并未出错, errgroup不会对返回的context进行取消操作
			if err != nil {
				log.Printf("服务二出错: %v", err)
			}
			return err
		}
	})

	// 等待所有服务完成,或者某个服务报错并终止所有服务
	err := g.Wait()
	log.Printf("程序退出:%v", err)
}