程序为什么需要优雅退出
原因很简单,我们都不希望自己的程序被异常关闭或者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的优雅之处亦在于此。
如果你觉得小编总结的不错,可以关注小编的公众号哦^_^