如何更干净地退出Go服务

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

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

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

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

用上下文处理取消信号

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

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

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

go 复制代码
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函数。我们可以通过调用它所返回的取消函数来控制它。
go 复制代码
ctx, cancel:= context.WithCancel(ctx)
cancel() 
go 复制代码
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通道被关闭时,以先发生的为准。

其使用的一个例子:

go 复制代码
ctx, stop := signal.NotifyContext(
context.Background(), os.Interrupt, syscall.SIGTERM)
  • os.Interrupt信号相当于按CTRL+C中断进程。
  • SIGTERM则相当于向程序发送一个通用的终止信号。

添加一个退出任务

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

go 复制代码
Last number from random sequence: 8

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

go 复制代码
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!")
}

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

go 复制代码
...
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:

go 复制代码
Last number from random sequence: 97

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

使用 WaitGroup

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

go 复制代码
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服务。否则,任何与之相连的开放连接都会被突然关闭,从用户体验的角度来看,这并不是好的实践。

go 复制代码
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()后,服务器实例将停止监听新的请求,然后关闭所有空闲的连接,然后无限期地等待连接恢复到空闲状态,然后关闭。

总结

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

相关推荐
Scott9999HH1 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
码智社2 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海2 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖3 小时前
JDK 26 新特性详解
java·开发语言
马优晨4 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区6 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大6 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai6 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
এ慕ོ冬℘゜7 小时前
前端基础:什么是时间戳?JS获取时间戳三种方法与实战用途
开发语言·前端·javascript
执明wa7 小时前
LayoutInflater详解: XML是如何变成View的?
android·xml·开发语言·android studio