程序为什么需要优雅退出

原因很简单,我们都不希望自己的程序被异常关闭或者ctrl+c给直接干掉,或许我们这回正在写数据库,或许正在完成一个复杂的计算流程;我们希望程序能在完成手头的工作之后才关闭,就好比编辑器退出是自动保存一样,防止之前的工作白费,更糟糕的是,导致异常或者不一致的数据,尤其是服务端开发的同学,一定要注意关闭服务器的时候要关闭数据库,服务监听,关闭文件等一系列操作。

实现方法

其实实现办法很简单,golang提供了现成的包来帮助我们解决这个问题。

第一种方法是通过信号解决这个问题:

 signal .Notify(c chan<- os.Signal, sig ...os.Signal)
 

第一个参数:一个 接受信号 的通道

其他参数:为需要捕获的系统信号的数组

当相关系统信号被触发时,c中将会有数据,可通过c来阻塞程序,来实现在接到系统信号后自定义处理逻辑;

运用channel实现优雅关闭程序完整Demo示例:

首先我们创建一个os.Signal channel,然后使用signal.Notify注册要接收的信号。

package main
import "fmt"
import "os"
import "os/signal"
import "syscall"
func main() {
 // Go signal notification works by sending `os.Signal`
 // values on a channel. We'll create a channel to
 // receive these notifications (we'll also make one to
 // notify us when the program can exit).
 sigs := make(chan os.Signal, 1)
 done := make(chan bool, 1)
 // `signal.Notify` registers the given channel to
 // receive notifications of the specified signals.
 signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
 // This goroutine executes a blocking receive for
 // signals. When it gets one it'll print it out
 // and then notify the program that it can finish.
 go func() {
 sig := <-sigs
 fmt.Println()
 fmt.Println(sig)
 done <- true
 }()
 // The program will wait here until it gets the
 // expected signal (as indicated by the goroutine
 // above sending a value on `done`) and then exit.
 fmt.Println("awaiting signal")
 <- do ne
 fmt.Println("exiting")
}
 

go run main.go执行这个程序,敲入ctrl-C会发送SIGINT信号。 此程序接收到这个信号后会打印退出。

其次,就是context的使用,用于在不同协程之间完成通信;要实现优雅退出,需要通过context将程序结束的消息通知到各个正在处理的协程,让他们做好退出准备(只处理手头的任务);

 ctx , cancel := context.WithCancel(context.Background())
 

context.WithCancel返回一个新的context和与之对应的cancel函数,调用cancel函数,将会降Done的信号通知到所有正在使用ctx的协程;

运用Context 实现优雅关闭程序完整Demo示例:

package main
​
import (
 "fmt"
 "sync"
)
​
func main() {
 var wg sync.WaitGroup
​
 do := make(chan int)
 done := make(chan int)
​
 for i := 0; i < 10; i++ {
 wg.Add(1)
​
 go func(i int) {
 defer wg.Done()
​
 select {
 case <-do:
 fmt.Printf("Work: %d\n", i)
 case <-done:
 fmt.Printf("Quit: %d\n", i)
 }
 }(i)
 }
​
  close (done)
​
 wg.Wait()
}
 

注意代码里的 done,它用来关闭 goroutines,实际使用非常简单,只要调用 close 即可,所有的 goroutines 都会收到关闭的消息。是不是很简单,如此说来,那为什么还要用 Context 控制 goroutine 的退出呢,它有什么特别的好处?实际上这是因为 Context 实现了继承,可以完成更复杂的操作,虽然我们自己编码也能实现,但是通过使用 Context,可以让代码更标准化一些。下面通过例子说明一下:

type userID string
​
func tree() {
 ctx1 := context.Background()
 ctx2, _ := context.WithCancel(ctx1)
 ctx3, _ := context.WithTimeout(ctx2, time.Second*5)
 ctx4, _ := context.WithTimeout(ctx3, time.Second*3)
 ctx5, _ := context.WithTimeout(ctx3, time.Second*6)
 ctx6 := context.WithValue(ctx5, userID("UserID"), 123)
​
 // ...
}
 

如此构造了 Context 继承链:

当 3s 超时后,ctx4 会被触发:

当 5s 超时后,ctx3 会被触发,不急如此,其子节点 ctx5 和 ctx6 也会被触发,即便 ctx5 本身的超时时间还没到,但因为它的父节点已经被触发了,所以它也会被触发:

总体来说,Context 是一个实战派的产物,虽然谈不上优雅,但是它已经是社区里的事实标准。实际使用中,任何有可能「慢」的方法都应该考虑通过 Context 实现退出机制,以避免因为无法退出导致泄露问题,对于服务端编程而言,通常意味着你很多方法的第一个参数都会是 Context。

总结

程序的优雅退出时相当重要的,对于维护数据的完整性至关重要,也是一种很好的编码习惯;上面的示例提供了一种实现的方式,但是不同的运用场景可能需要更加细致的考虑;同时,也没办法处理kill -9这样暴力的关闭进程的场景。

一句话总结:用Go编程就像在创作艺术,Go的优雅之处亦在于此。

如果你觉得小编总结的不错,可以关注小编的公众号哦^_^