Go1.16 中的新函數(shù) signal.NotifyContext 怎么用?
閱讀本文大概需要 4 分鐘。
大家好,我是 polarisxu。
os/signal 這個(gè)包大家可能用的不多。但自從 Go1.8 起,有些人開始使用這個(gè)包了,原因是 Go1.8 在 net/http 包新增了一個(gè)方法:
func (srv *Server) Shutdown(ctx context.Context) error
有了它就不需要借助第三方庫實(shí)現(xiàn)優(yōu)雅關(guān)閉服務(wù)了。具體怎么做呢?
func main() {
server = http.Server{
Addr: ":8080",
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 10)
fmt.Fprint(w, "Hello world!")
})
go server.ListenAndServe()
// 監(jiān)聽中斷信號(hào)(CTRL + C)
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-c
// 重置 os.Interrupt 的默認(rèn)行為
signal.Reset(os.Interrupt)
fmt.Println("shutting down gracefully, press Ctrl+C again to force")
// 給程序最多 5 秒時(shí)間處理正在服務(wù)的請求
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(timeoutCtx); err != nil {
fmt.Println(err)
}
}
這里利用 os/signal 包監(jiān)聽 Interrupt 信號(hào); 收到該信號(hào)后,16 行 <-c會(huì)返回;為了可以再次 CTRL + C 強(qiáng)制退出,通過 Reset 恢復(fù) os.Interrupt 的默認(rèn)行為;(這不是必須的)
優(yōu)雅退出的關(guān)鍵:1)新請求進(jìn)不來;2)已有請求給時(shí)間處理完。所以,在接收到信號(hào)后,調(diào)用 server.Shutdown 方法,阻止新請求進(jìn)來,同時(shí)給 5 秒等待時(shí)間,讓已經(jīng)進(jìn)來的請求有時(shí)間處理。
在 Go1.16 中,os/signal 包新增了一個(gè)函數(shù):
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc)
功能和 Notify 類似,但用法上有些不同。上面的例子改用 NotifyContext:
func after() {
server = http.Server{
Addr: ":8080",
}
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
time.Sleep(time.Second * 10)
fmt.Fprint(w, "Hello world!")
})
go server.ListenAndServe()
// 監(jiān)聽中斷信號(hào)(CTRL + C)
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt)
<-ctx.Done()
// 重置 os.Interrupt 的默認(rèn)行為,類似 signal.Reset
stop()
fmt.Println("shutting down gracefully, press Ctrl+C again to force")
// 給程序最多 5 秒時(shí)間處理正在服務(wù)的請求
timeoutCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(timeoutCtx); err != nil {
fmt.Println(err)
}
}
和上面的寫法有區(qū)別,完成的功能一樣的。其實(shí) NotifyContext 的內(nèi)部就是基于 Notify 實(shí)現(xiàn)的:
func NotifyContext(parent context.Context, signals ...os.Signal) (ctx context.Context, stop context.CancelFunc) {
ctx, cancel := context.WithCancel(parent)
c := &signalCtx{
Context: ctx,
cancel: cancel,
signals: signals,
}
c.ch = make(chan os.Signal, 1)
Notify(c.ch, c.signals...)
if ctx.Err() == nil {
go func() {
select {
case <-c.ch:
c.cancel()
case <-c.Done():
}
}()
}
return c, c.stop
}
只是在返回的 stop 被調(diào)用時(shí),會(huì)執(zhí)行 os/signal 包中的 Stop 函數(shù),這個(gè) Stop 函數(shù)的功能和 Reset 類似。因此上面 Notify 的例子,Reset 的地方可以改為 Stop。
從封裝上看,NotifyContext 做的更好。而且,如果在某些需要 Context 的場景下,它把監(jiān)控系統(tǒng)信號(hào)和創(chuàng)建 Context 一步搞定。
NotifyContext 的用法,優(yōu)雅的關(guān)閉服務(wù),你掌握了嗎?希望你實(shí)際動(dòng)手試驗(yàn)下,啟動(dòng)服務(wù),通過 curl http://localhost:8080/ 訪問,然后按 CTRL + C,看看具體效果。只看不動(dòng)手,基本知識(shí)不是你的。
關(guān)于 NotifyContext 函數(shù)的文檔可以在這里查看:https://docs.studygolang.com/pkg/os/signal/#NotifyContext。
推薦閱讀
