如何更干净地退出Go服务

简介: 如何更干净地退出Go服务

Go的主要特点之一是能够并发运行多个任务。它是一种因其通用性、快速执行和易于使用而广泛使用的编程语言。


它可以用于开发网络服务器或其他长期运行的任务。在这些情况下,能够优雅地关闭应用程序是很重要的,特别是在处理某种状态信息的时候。如果有必要的话,可以清理已使用的资源,并顺利地终止其并发的进程。


优雅关机的概念通常是在操作系统的背景下使用的。与之相反的是强制关机,即系统在关机前没有机会执行它应该执行的任务就关机了。


幸运的是,Go提供了一些工具,使我们能够监听关机请求并对其进行相应的处理。


用上下文处理取消信号


在运行多个Go协程时,我们需要为它们提供一种协调的方式来顺利退出。这可以通过Context来实现。根据它的文档:


context定义了Context类型,它携带了deadlinecancel和其他跨API边界和进程间的请求范围值。


让我们考虑下面这个随机序列的例子:


func pushSequence() <-chan int {
  clock := time.NewTicker(2 * time.Second)
  sequencePusher := make(chan int)
  go func () {
    var i int
    for range clock.C {
      i = rand.Intn(120)
      sequencePusher <- i
    }
  }()
  return sequencePusher
}
func main() {
  sequenceChannel := pushSequence()
  for i := range sequenceChannel {
    fmt.Printf("Received: %v \n", i)
    if i > 90 {
  break
    }
  }
  
  fmt.Println("Random sequence finished. Starting next task! ")
  // Continuing the program
  nextTask()
}


主函数的for循环将从channel中接收数值,并在其中一个数值大于90的时候中断。问题是,即使我们已经从循环中退出,协程仍将继续尝试在后台向channel写入,而实际上我们正在运行下一个任务。


这就是我们的程序中的一个漏洞!


那么,在实践中,go程序没有办法知道我们不再需要它了,它仍然会被挂在那里。我们需要一种方法来向go程序传达它可以退出,并且我们不再需要它运行。在像这样的小程序中,这并不是什么大问题,但随着我们的应用程序的增长和变得更加复杂,这可能是一个问题。


只是要采取一些要点,我们要用这些要点来解决这个问题:


  • context.Background函数允许我们在启动一个新的进程时启动一个新的上下文。
  • Context接口暴露了一个返回只读通道的Done方法,该方法可以作为取消信号使用。
  • 通过context.WithCancel函数。我们可以通过调用它所返回的取消函数来控制它。


ctx, cancel:= context.WithCancel(ctx)
cancel()
func pushSequence(ctx context.Context) <-chan int {
  clock := time.NewTicker(2 * time.Second)
  sequencePusher := make(chan int)
  go func () {
    var i int
    for {
      select {
      case <-clock.C:
        i = rand.Intn(150)
        sequencePusher <- i
      case <-ctx.Done():
        close(sequencePusher)
        fmt.Println("Closing the sequence")
        return
      }
    }
  }()
  return sequencePusher
}
func main() {
  ctx, cancel := context.WithCancel(context.Background())
  sequenceChannel := pushSequence(ctx)
  for i := range sequenceChannel {
    fmt.Printf("Received: %v \n", i)
    if i > 90 {
  break
    }
  }
  cancel()
  fmt.Println("Random sequence finished. Starting next task! ")
  // Continuing the program
  nextTask()
}


协程将退出,而不是试图在后台写到channel。我们将不再让它在我们的程序中的其他任务中徘徊。

监听操作系统的信号


现在是实现优雅退出程序的第二部分,也就是监听操作系统的信号。特别是那些负责中断或终止应用程序的信号。

os/signal包允许我们在关闭程序之前监听并处理它们。


我们可以通过使用NotifyContext函数将这个功能与上下文的使用结合起来:


NotifyContext返回一个父级上下文的副本,当其中一个列出的信号到达时,当返回的停止函数被调用时,或者当父级上下文的Done通道被关闭时,以先发生的为准。


其使用的一个例子:


ctx, stop := signal.NotifyContext(
context.Background(), os.Interrupt, syscall.SIGTERM)


  • os.Interrupt信号相当于按CTRL+C中断进程。
  • SIGTERM则相当于向程序发送一个通用的终止信号。


添加一个退出任务


让我们在我们的随机序列发生器上加入一个关闭任务。每当我们从程序中退出时,我们要把从序列中收到的最后一个数字写到一个文件中:


Last number from random sequence: 8


我们可以结合本文到目前为止所看到的工具来实现它。


func pushSequence(ctx context.Context) <-chan int {
  clock := time.NewTicker(2 * time.Second)
  sequencePusher := make(chan int)
  go func () {
    var i int
    for {
      select {
        case <-clock.C:
          i = rand.Intn(120)
          sequencePusher <- i
        case <-ctx.Done(): // Activated when ctx.Done() closes
          fmt.Println("Closing sequence")
          close(sequencePusher)
          if err := writeLastFromSequence(i); err != nil {
            log.Fatal(err)
          }
          return
        }
      }
  }()
  return sequencePusher
}
func main() {
  // Listening to the OS Signals
  ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  defer stop()
  randSequence := pushSequence(ctx)
  for i := range randSequence {
    fmt.Printf("Received: %v \n", i)
  }
  fmt.Println("I'm leaving, bye!")
}


我们的程序将无限期地监听这个随机序列,直到我们中断或终止程序。


...
Received: 54 
Received: 97 
Received: 103 
Received: 76 
Received: 20 
Received: 63 
Received: 33 
Received: 97 
^CClosing sequence
I'm leaving, bye!


在同一个文件夹中,我们可以看到生成的文件last-num-from-seq.txt


Last number from random sequence: 97


我们有了,从序列中收到的最后一个数字已经成功保存。


使用 WaitGroup


我注意到,在某些情况下,主函数可以在其他协程有时间执行清理任务之前完成。为了避免这个问题,我们可以引入一个WaitGroup,以确保我们要等到所有的清理工作都正常完成:


func pushSequence(ctx context.Context, wg *sync.WaitGroup) <-chan int {
  clock := time.NewTicker(2 * time.Second)
  sequencePusher := make(chan int)
  go func () {
    defer wg.Done()
    var i int
    for {
      select {
        case <-clock.C:
          i = rand.Intn(120)
          sequencePusher <- i
        case <-ctx.Done(): // Activated when ctx.Done() closes
          fmt.Println("Closing sequence")
          close(sequencePusher)
          if err := writeLastFromSequence(i); err != nil {
            log.Fatal(err)
          }
          return
        }
      }
  }()
  return sequencePusher
}
func main() {
  // Listening to the OS Signals
  ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  defer stop()
 
  var wg sync.WaitGroup
  wg.Add(1)
  randSequence := pushSequence(ctx, &wg)
  for i := range randSequence {
    fmt.Printf("Received: %v \n", i)
  }
  wg.Wait()
  fmt.Println("I'm leaving, bye!")
}


现在我们可以确定,当程序退出时,该文件将被写入。

安全地退出服务


使用这些工具的一个常见方法是在GO中优雅地关闭一个Web服务。否则,任何与之相连的开放连接都会被突然关闭,从用户体验的角度来看,这并不是好的实践。


func main() {
  ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
  defer stop()
  var wg sync.WaitGroup
  server := &http.Server{
    Addr:    ":3333",
  }
  wg.Add(2)
  go func () {
    defer wg.Done()
    if err := server.ListenAndServe(); err != http.ErrServerClosed {
      log.Fatal(err)
    }
  }()
  go func () {
    defer wg.Done()
    <-ctx.Done()
    log.Println("Closing HTTP Server")
    if err := server.Shutdown(context.Background()); err != nil {
      log.Fatal(err)
    }
  }()
  wg.Wait()
  fmt.Println("I'm leaving, bye!")
}


根据其文档,在调用server.Shutdown()后,服务器实例将停止监听新的请求,然后关闭所有空闲的连接,然后无限期地等待连接恢复到空闲状态,然后关闭。


总结


在这篇文章中,我们看到了使用 Contextos/signal 包来关闭 Golang 应用程序的另一种方式,同时看到了一些如何应用它们的例子。它们可以单独使用,也可以结合起来执行清理工作,或者只是避免留下泄漏的协程。这是用Golang开发一个坚实可靠的应用程序的重要基石之一。

相关文章
|
10月前
|
JSON 中间件 Go
Go 网络编程:HTTP服务与客户端开发
Go 语言的 `net/http` 包功能强大,可快速构建高并发 HTTP 服务。本文从创建简单 HTTP 服务入手,逐步讲解请求与响应对象、URL 参数处理、自定义路由、JSON 接口、静态文件服务、中间件编写及 HTTPS 配置等内容。通过示例代码展示如何使用 `http.HandleFunc`、`http.ServeMux`、`http.Client` 等工具实现常见功能,帮助开发者掌握构建高效 Web 应用的核心技能。
486 61
|
10月前
|
开发框架 安全 前端开发
Go Web开发框架实践:模板渲染与静态资源服务
Gin 是一个功能强大的 Go Web 框架,不仅适用于构建 API 服务,还支持 HTML 模板渲染和静态资源托管。它可以帮助开发者快速搭建中小型网站,并提供灵活的模板语法、自定义函数、静态文件映射等功能,同时兼容 Go 的 html/template 引擎,具备高效且安全的页面渲染能力。
|
10月前
|
开发框架 JSON 中间件
Go语言Web开发框架实践:使用 Gin 快速构建 Web 服务
Gin 是一个高效、轻量级的 Go 语言 Web 框架,支持中间件机制,非常适合开发 RESTful API。本文从安装到进阶技巧全面解析 Gin 的使用:快速入门示例(Hello Gin)、定义 RESTful 用户服务(增删改查接口实现),以及推荐实践如参数校验、中间件和路由分组等。通过对比标准库 `net/http`,Gin 提供更简洁灵活的开发体验。此外,还推荐了 GORM、Viper、Zap 等配合使用的工具库,助力高效开发。
|
缓存 弹性计算 API
用 Go 快速开发一个 RESTful API 服务
用 Go 快速开发一个 RESTful API 服务
|
存储 JSON Go
PHP 日志系统的最佳搭档:一个 Go 写的远程日志收集服务
为了不再 SSH 上去翻日志,我写了个 Go 小脚本,用来接收远程日志。PHP 负责记录日志,Go 负责存储和展示,按天存储、支持 API 访问、可远程管理,终于能第一时间知道项目炸了。
297 10
|
Go UED
Go Web服务中如何优雅平滑重启?
在生产环境中,服务升级时如何确保不中断当前请求并应用新代码是一个挑战。本文介绍了如何使用 Go 语言的 `endless` 包实现服务的优雅重启,确保在不停止服务的情况下完成无缝升级。通过示例代码和测试步骤,详细展示了 `endless` 包的工作原理和实际应用。
423 3
|
JSON Go UED
Go Web服务中如何优雅关机?
在构建 Web 服务时,优雅关机是一个关键的技术点,它确保服务关闭时所有正在处理的请求都能顺利完成。本文通过一个简单的 Go 语言示例,展示了如何使用 Gin 框架实现优雅关机。通过捕获系统信号和使用 `http.Server` 的 `Shutdown` 方法,我们可以在服务关闭前等待所有请求处理完毕,从而提升用户体验,避免数据丢失或不一致。
309 1
|
Go API 开发者
深入探讨:使用Go语言构建高性能RESTful API服务
在本文中,我们将探索Go语言在构建高效、可靠的RESTful API服务中的独特优势。通过实际案例分析,我们将展示Go如何通过其并发模型、简洁的语法和内置的http包,成为现代后端服务开发的有力工具。
|
XML JSON Go
Swoole与Go系列教程之WebSocket服务的应用
在 WebSocket 协议出现之前,Web 应用为了能过获取到实时的数据都是通过不断轮询服务端的接口。轮询的效率、延时很低,并且很耗费资源。
1226 2
Swoole与Go系列教程之WebSocket服务的应用
|
网络协议 Go
Swoole与Go系列教程之TCP服务的应用
TCP(传输控制协议)的出现是为了解决计算机网络中的数据可靠传输和连接管理的问题。在早期的计算机网络中,特别是在分组交换和互联网的发展初期,网络是不可靠的,存在丢包、错误和延迟等问题。
1196 0
Swoole与Go系列教程之TCP服务的应用

热门文章

最新文章

下一篇
开通oss服务