当前位置: 首页 > news >正文

Go 实现程序优雅退出

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

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

HTTP Server 平滑关闭

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

案例一: 

package mainimport ("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
}

案例二:持续监听 

package mainimport ("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 方法实现  

package mainimport ("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() {<-sigsfmt.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 文件定义如下:

syntax = "proto3";package example;// The greeter service definition.
service Greeter {// Sends a greetingrpc 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 代码:

protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative your_service.proto

 创建服务实现:

package mainimport ("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
}

 在主函数中注册服务:

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 实现  

package mainimport ("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收到停机信号")returndefault:// 执行工作任务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 方法进行平滑关闭 

 

package mainimport ("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")

package mainimport ("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("服务器正常关闭")
}
package mainimport ("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,}// 创建一个用于通知服务器关闭的channeldone := 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("服务器已退出")
}

 

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 小阿轩yx-Shell 编程规范与变量
  • 集合、Collection接口特点和常用方法
  • 计算机SCI期刊,IF=8+,专业性强,潜力新刊!
  • 基于高通公司AI Hub Models的On-Device AI学习:Introduction to On-Device AI
  • k8s证书过期处理 手动生成证书、凭证
  • Google Find My Device:科技守护,安心无忧
  • Java入门基础学习笔记50——ATM系统
  • 怎么判断同步时序逻辑电路和异步时序逻辑电路?
  • 【字典树 马拉车算法】336. 回文对
  • [硬件笔记] IIC通讯、开漏输出、上拉电阻
  • 数据结构面试题总结
  • 8.继承和多态
  • C# 中的 Dictionary<TKey, TValue> 类
  • vue3 input输入框输入限制(数字)
  • Python学习(3) 函数
  • 9月CHINA-PUB-OPENDAY技术沙龙——IPHONE
  • 【399天】跃迁之路——程序员高效学习方法论探索系列(实验阶段156-2018.03.11)...
  • dva中组件的懒加载
  • exports和module.exports
  • HTTP请求重发
  • iOS | NSProxy
  • JavaScript异步流程控制的前世今生
  • jQuery(一)
  • Js基础——数据类型之Null和Undefined
  • node 版本过低
  • Perseus-BERT——业内性能极致优化的BERT训练方案
  • React+TypeScript入门
  • SQLServer之创建显式事务
  • Vue小说阅读器(仿追书神器)
  • vue自定义指令实现v-tap插件
  • ------- 计算机网络基础
  • 前端路由实现-history
  • MiKTeX could not find the script engine ‘perl.exe‘ which is required to execute ‘latexmk‘.
  • 《TCP IP 详解卷1:协议》阅读笔记 - 第六章
  • Hibernate主键生成策略及选择
  • 仓管云——企业云erp功能有哪些?
  • ​MySQL主从复制一致性检测
  • ​数据结构之初始二叉树(3)
  • #在 README.md 中生成项目目录结构
  • $GOPATH/go.mod exists but should not goland
  • $NOIp2018$劝退记
  • (1)(1.13) SiK无线电高级配置(六)
  • (3)(3.5) 遥测无线电区域条例
  • (4) openssl rsa/pkey(查看私钥、从私钥中提取公钥、查看公钥)
  • (7)摄像机和云台
  • (大众金融)SQL server面试题(1)-总销售量最少的3个型号的车及其总销售量
  • (多级缓存)缓存同步
  • (二开)Flink 修改源码拓展 SQL 语法
  • (分布式缓存)Redis持久化
  • (附源码)springboot宠物医疗服务网站 毕业设计688413
  • (论文阅读40-45)图像描述1
  • (三)uboot源码分析
  • (一)u-boot-nand.bin的下载
  • (转)真正的中国天气api接口xml,json(求加精) ...
  • *1 计算机基础和操作系统基础及几大协议