Go 实现程序优雅退出

在Go语言中,实现程序的优雅退出是一项重要的任务,特别是在涉及到HTTP服务器、gRPC服务器、以及其他后台工作的情况下。

在实际应用中,通常建议同时监听 os.Interruptsyscall.SIGTERM,因为它们都是常见的终止信号,可以确保你的程序能够优雅地响应不同的关闭场景。例如,在生产环境中,系统管理员可能会使用 SIGTERM 来终止服务,而不是依赖于 Ctrl+C

HTTP Server 平滑关闭

Go 1.8及以上版本提供了 http.Server 结构的 Shutdown 方法,用于平滑关闭HTTP服务器。

案例一:

Go 复制代码
package main

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

func main() {
	// 创建一个新的 ServeMux 对象,它是HTTP请求多路复用器,用于将不同的请求路由到不同的处理函数
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

	server := &http.Server{
		Addr:    ":8088", //监听端口
		Handler: mux,     //监听的处理器
	}

	//监听并服务HTTP请求,可以想办法开一个8088端口来占用,比如在java起一个服务
	go func() {
		if err := server.ListenAndServe(); err != nil {
			if err != http.ErrServerClosed {
				// 处理监听失败的错误
				// 记录错误
				log.Printf("HTTP服务器失败: %v", err)

				// 执行清理工作,如有必要

				// 可选:尝试重启服务器
				time.Sleep(10 * time.Second) //等待10秒再重启
				if !attemptRestart(server) {
					//os.Exit(1) 将导致程序立即退出,并返回状态码 1 表示发生了错误。在实际应用中,你可能需要根据错误的性质和程序的设计来决定是否退出程序,或者采取其他的错误恢复策略
					// 优雅地退出程序
					os.Exit(1)
				}
			}
		}
	}()

	// 等待中断信号来优雅地关闭服务器
	stop := make(chan os.Signal, 1)
	// 用 signal.Notify 来监听 os.Interrupt 信号,这是用户向程序发送中断信号(如Ctrl+C)时产生的信号
	signal.Notify(stop, os.Interrupt)

	<-stop // 程序在此处阻塞,直到接收到一个中断信号

	//当有中断信号来,创建一个带有超时的 context.Context 对象,超时时间为5秒
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	// 确保在函数返回时取消这个上下文,释放相关资源
	defer cancel()

	//当接收到中断信号时,调用 server.Shutdown 方法并传入上面创建的 ctx 对象,以优雅地关闭服务器
	if err := server.Shutdown(ctx); err != nil {
		// 如果在关闭过程中出现错误
		fmt.Println("处理关闭服务器时的错误")
	}
}

// attemptRestart 尝试重启服务器
func attemptRestart(server *http.Server) bool {
	// 这里可以添加任何需要的清理或重启前的准备工作
	log.Println("正在尝试重新启动服务器。。。")

	// 尝试重新启动服务器
	err := server.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		log.Printf("无法重新启动服务器: %v", err)
		return false
	}
	log.Println("重启成功。。。")
	return true
}

案例二:持续监听

Go 复制代码
package main

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

func main() {
	// 创建一个新的 ServeMux 对象,它是HTTP请求多路复用器,用于将不同的请求路由到不同的处理函数
	mux := http.NewServeMux()
	mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
		w.Write([]byte("Hello, World!"))
	})

	server := &http.Server{
		Addr:    ":8088", //监听端口
		Handler: mux,     //监听的处理器
	}

	go func() {
		// 等待中断信号来优雅地关闭服务器
		stop := make(chan os.Signal, 1)
		// 用 signal.Notify 来监听 os.Interrupt 信号,这是用户向程序发送中断信号(如Ctrl+C)时产生的信号
		signal.Notify(stop, os.Interrupt)

		<-stop // 程序在此处阻塞,直到接收到一个中断信号

		//当有中断信号来,创建一个带有超时的 context.Context 对象,超时时间为5秒
		ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
		// 确保在函数返回时取消这个上下文,释放相关资源
		defer cancel()

		//当接收到中断信号时,调用 server.Shutdown 方法并传入上面创建的 ctx 对象,以优雅地关闭服务器
		if err := server.Shutdown(ctx); err != nil {
			// 如果在关闭过程中出现错误
			fmt.Println("处理关闭服务器时的错误")
		}
	}()

	//监听并服务HTTP请求,可以想办法开一个8088端口来占用,比如在java起一个服务
	for {
		if err := server.ListenAndServe(); err != nil {
			if err != http.ErrServerClosed {
				// 处理监听失败的错误
				// 记录错误
				log.Printf("HTTP服务器失败: %v", err)

				// 执行清理工作,如有必要

				// 可选:尝试重启服务器
				time.Sleep(5 * time.Second) //等待10秒再重启
				attemptRestart(server)

				//os.Exit(1) 将导致程序立即退出,并返回状态码 1 表示发生了错误。在实际应用中,你可能需要根据错误的性质和程序的设计来决定是否退出程序,或者采取其他的错误恢复策略
				// 优雅地退出程序
				//os.Exit(1)
			}
		}
	}
}

// attemptRestart 尝试重启服务器
func attemptRestart(server *http.Server) bool {
	// 这里可以添加任何需要的清理或重启前的准备工作
	log.Println("正在尝试重新启动服务器。。。")

	// 尝试重新启动服务器
	err := server.ListenAndServe()
	if err != nil && err != http.ErrServerClosed {
		log.Printf("无法重新启动服务器: %v", err)
		return false
	}
	return true
}
gRPC Server 平滑关闭

gRPC服务器的平滑关闭可以通过 GracefulStop 方法实现

Go 复制代码
package main

import (
	"fmt"
	"google.golang.org/grpc"
	"google.golang.org/grpc/reflection"
	"log"
	"net"
	"os"
	"os/signal"
	"syscall"
)

func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("监听失败: %v", err)
	}
	s := grpc.NewServer()
	// 注册服务...(需要自己写)

	// 在gRPC服务上启用反射服务
	//启用反射服务后,客户端可以使用 gRPC 反射 API 查询服务器支持的服务列表、服务下的方法列表等信息。
	//这对于开发和测试阶段非常有用,因为它允许客户端在没有预先定义 .proto 文件的情况下与服务器通信。
	reflection.Register(s)

	// 监听系统关闭信号
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigs
		fmt.Println("收到停止信号,正在正常停止gRPC服务器。。。")
		s.GracefulStop() // 调用 GracefulStop 方法来平滑关闭服务器
	}()

	//如果 s.Serve(lis) 调用成功,服务器将正常运行,等待和处理客户端的请求。
	//如果发生错误,比如监听出现问题或者服务器无法处理请求,err 变量将包含相应的错误信息
	if err := s.Serve(lis); err != nil {
		log.Fatalf("服务失败: %v", err)
	}

	//go func() {
	//	if err := s.Serve(lis); err != nil {
	//		// 处理gRPC服务启动错误
	//	}
	//}()
}

在 gRPC 中,注册服务是指将服务的实现与 gRPC 服务器关联起来。在 Go 语言中,这通常通过调用服务接口的 RegisterXXXServer 方法来完成,其中 XXX 是服务名称。以下是注册服务的一般步骤:

  1. 定义服务接口 : 首先,你需要定义服务接口,这通常在 .proto 文件中完成。例如,如果你有一个名为 Greeter 的服务,它将包含一个 SayHello 方法。

  2. 生成服务代码 : 使用 protoc 编译器和 gRPC 插件为 Go 生成服务代码。这将生成两个文件:<service_name>.pb.go<service_name>_grpc.pb.go。第一个文件包含消息类型的定义,第二个文件包含服务接口的定义。

  3. 创建服务实现 : 创建一个结构体来实现服务接口。这个结构体需要实现 .proto 文件中定义的所有方法。

  4. 注册服务: 在你的主函数中,创建一个 gRPC 服务器实例,并使用生成的服务注册函数将服务实现注册到服务器上。

下面是一个简单的示例,演示了如何注册一个名为 Greeter 的服务:

假设你的 .proto 文件定义如下:

Go 复制代码
syntax = "proto3";

package example;

// The greeter service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings.
message HelloReply {
  string message = 1;
}

生成 Go 代码:

Go 复制代码
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative your_service.proto

创建服务实现:

Go 复制代码
package main

import (
	"context"
	"log"

	"google.golang.org/grpc"
	pb "path/to/your_package" // 替换为你的包路径
)

// server 是 GreeterServer 的实现。
type server struct {
	pb.UnimplementedGreeterServer
}

// SayHello 实现 GreeterServer 的 SayHello 方法。
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
	log.Printf("Received: %v", in.GetName())
	return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

在主函数中注册服务:

Go 复制代码
func main() {
	lis, err := net.Listen("tcp", ":50051")
	if err != nil {
		log.Fatalf("failed to listen: %v", err)
	}
	defer lis.Close()

	s := grpc.NewServer()
	pb.RegisterGreeterServer(s, &server{}) // 注册服务

	if err := s.Serve(lis); err != nil {
		log.Fatalf("failed to serve: %v", err)
	}
}

在这个示例中,pb.RegisterGreeterServer 是由 protoc 生成的函数,用于将 server 实例注册到 gRPC 服务器上。pb 是你的包名,它是由 protoc 编译器根据 .proto 文件的包声明生成的。

请确保将 "path/to/your_package" 替换为实际的包路径,这个路径指向包含你的 .proto 文件生成的 Go 代码的位置。

Worker 协程平滑关闭

对于worker协程的平滑关闭,可以使用 context.Context 实现

Go 复制代码
package main

import (
	"context"
	"fmt"
	"os"
	"os/signal"
	"sync"
	"time"
)

func worker(ctx context.Context, wg *sync.WaitGroup) {
	defer wg.Done()

	for {
		select {
		case <-ctx.Done():
			fmt.Println("worker收到停机信号")
			return
		default:
			// 执行工作任务
			fmt.Println("Working...")
			time.Sleep(time.Second)
		}
	}
}

func main() {
	ctx, cancel := context.WithCancel(context.Background())
	var wg sync.WaitGroup

	// 启动worker协程
	wg.Add(1)
	go worker(ctx, &wg)

	// 等待中断信号来优雅地关闭worker协程
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt)

	<-stop // 等待中断信号
	fmt.Println("正在关闭。。。")

	// 发送关闭信号给worker协程
	cancel()
	wg.Wait()
	fmt.Println("关闭完成")
}
实现 `io.Closer` 接口的自定义服务平滑关闭

实现 io.Closer 接口的服务可以通过调用 Close 方法进行平滑关闭

Go 复制代码
package main

import (
	"fmt"
	"os"
	"os/signal"
	"sync"
)

type MyService struct {
	mu sync.Mutex
	// 其他服务相关的字段
}

// MyService 实现了 Close 方法,那么它就隐式地实现了 io.Closer 接口
func (s *MyService) Close() error {
	s.mu.Lock()
	defer s.mu.Unlock()

	// 执行关闭服务的操作
	fmt.Println("正在关闭MyService。。。")
	return nil
}

func main() {
	service := &MyService{}

	// 等待中断信号来优雅地关闭服务
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt)

	<-stop // 等待中断信号
	fmt.Println("正在关闭。。。")

	// 调用Close方法进行平滑关闭
	if err := service.Close(); err != nil {
		fmt.Println("关闭服务时出错:", err)
	}

	fmt.Println("关闭完成")
}

以上是一些Golang中实现程序优雅退出的方法,具体的实现方式取决于你的应用程序结构和使用的库。在实际应用中,你可能需要组合使用这些方法以确保整个应用程序在退出时都能够平滑关闭。

在实际项目中的应用(Gin)

平滑关闭会阻止新的请求进来,并等待目前正在进行的业务处理完成(此处取决于timeout设置的时间,如果设置的时间过短,请求未完成,就会"服务器被迫关闭:context deadline exceeded")

Go 复制代码
package main

import (
	"context"
	"fmt"
	"github.com/gin-gonic/gin"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"
)

func main() {
	r := gin.Default()

	// 定义路由
	r.GET("/ping", func(c *gin.Context) {
		time.Sleep(5 * time.Second) //模拟业务处理时间
		c.String(http.StatusOK, "pong")
	})

	// 创建 HTTP 服务器
	srv := &http.Server{Addr: ":8080", Handler: r}

	// 等待中断信号来优雅地关闭服务
	stop := make(chan os.Signal, 1)
	signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

	// 在新的 Goroutine 中启动 HTTP 服务器
	go func() {
		if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
			fmt.Printf("listen: %v\n", err)
			os.Exit(1)
		}
	}()

	// 阻塞直到接收到停止信号
	<-stop

	// 创建一个 10 秒的超时上下文
	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// 优雅关闭 HTTP 服务器
	if err := srv.Shutdown(ctx); err != nil {
		fmt.Printf("无法正常关闭服务器: %v\n", err)
	}

	fmt.Println("服务器正常关闭")
}
Go 复制代码
package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"syscall"
	"time"

	"github.com/gin-gonic/gin"
)

func main() {
	r := gin.Default()

	// 添加路由等设置...
	r.GET("/ping", func(c *gin.Context) {
		time.Sleep(5 * time.Second)
		c.JSON(http.StatusOK, gin.H{
			"message": "pong",
		})
	})

	// 启动HTTP服务器
	srv := &http.Server{
		Addr:    ":8080",
		Handler: r,
	}

	// 创建一个用于通知服务器关闭的channel
	done := make(chan bool)

	go func() {
		// 监听中断信号,通常是Ctrl+C或Kill命令
		sig := make(chan os.Signal, 1)
		signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
		<-sig // 等待信号

		// 收到信号后,给出日志提示
		log.Println("Shutdown Server ...")

		// 调用Server的Shutdown方法,传入一个有超时上下文
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		if err := srv.Shutdown(ctx); err != nil {
			log.Fatal("服务器被迫关闭:", err)
		}
		close(done)
	}()

	// 启动HTTP服务
	log.Println("正在端口8080上启动服务器。。。")
	if err := srv.ListenAndServe(); err != http.ErrServerClosed {
		log.Fatalf("listen: %s\n", err)
	}

	<-done // 等待直到shutdown完成
	log.Println("服务器已退出")
}
相关推荐
上海_彭彭18 分钟前
【提效工具开发】Python功能模块执行和 SQL 执行 需求整理
开发语言·python·sql·测试工具·element
3345543226 分钟前
element动态表头合并表格
开发语言·javascript·ecmascript
沈询-阿里30 分钟前
java-智能识别车牌号_基于spring ai和开源国产大模型_qwen vl
java·开发语言
Yaml442 分钟前
智能化健身房管理:Spring Boot与Vue的创新解决方案
前端·spring boot·后端·mysql·vue·健身房管理
残月只会敲键盘44 分钟前
面相小白的php反序列化漏洞原理剖析
开发语言·php
ac-er88881 小时前
PHP弱类型安全问题
开发语言·安全·php
ac-er88881 小时前
PHP网络爬虫常见的反爬策略
开发语言·爬虫·php
爱吃喵的鲤鱼1 小时前
linux进程的状态之环境变量
linux·运维·服务器·开发语言·c++
DARLING Zero two♡1 小时前
关于我、重生到500年前凭借C语言改变世界科技vlog.16——万字详解指针概念及技巧
c语言·开发语言·科技