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开发一个坚实可靠的应用程序的重要基石之一。