Go 实现程序优雅退出

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

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

HTTP Server 平滑关闭

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

案例一: 

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
}

案例二:持续监听 

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

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 文件定义如下:

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 代码:

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

 创建服务实现:

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
}

 在主函数中注册服务:

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 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 方法进行平滑关闭 

 

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")

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("服务器正常关闭")
}
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("服务器已退出")
}

 

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要在 go-streams 中增加程序退出检测,可以使用 Go 语言中的信号处理机制。具体做法是,在程序启动时设置一个信号处理函数,当收到指定的信号时,程序就会执行相应的操作,比如退出程序。 首先,我们需要导入 `os` 和 `os/signal` 两个包,分别用于处理操作系统信号和 Go 语言信号。然后,定义一个 `done` 通道,用于传递程序退出的信号。最后,在程序启动时,设置一个信号处理函数,当收到 `os.Interrupt` 信号时,向 `done` 通道发送一个信号,表示程序退出。 示例代码如下: ```go package main import ( "fmt" "os" "os/signal" ) func main() { done := make(chan bool, 1) // 设置信号处理函数 signal.Notify(make(chan os.Signal, 1), os.Interrupt) go func() { <-done fmt.Println("Program exit.") // 执行清理操作 os.Exit(0) }() // 程序主体部分 // ... // 等待程序退出信号 <-done } ``` 在上面的代码中,我们使用了一个无缓冲的 `done` 通道来传递程序退出信号。在 `main()` 函数中,我们设置了一个信号处理函数,当接收到 `os.Interrupt` 信号时,向 `done` 通道发送一个信号,表示程序退出。然后,在程序主体部分执行完毕后,我们等待程序退出信号,即从 `done` 通道中接收一个信号,此时程序就会退出,并执行清理操作。 通过这种方法,我们可以在 go-streams 中增加程序退出检测,保证程序在收到指定的信号时能够正确地退出

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值