for { // 从通道接受信号,期间一直阻塞 i := <-c switch i { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: fmt.Println("receive exit signal ", i.String(), ",exit...") exit() os.Exit(0) } }
server run on: 127.0.0.1:8060 # mac/linux 上按Ctrl+C,windows上调试运行,然后点击停止 receive exit signal interrupt ,exit... Process finished with exit code 2
至此,我们就实现了所谓的优雅退出了,简单吧?
实战
封装
为了方便在多个项目中使用,建议在公共pkg包中新建对应的文件,封装进去,便于使用,下面是一个实现。
新建 `signal.go`:
package osutils import ( "fmt" "os" "os/signal" "syscall" ) // WaitExit will block until os signal happened func WaitExit(c chan os.Signal, exit func()) { for i := range c { switch i { case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT: fmt.Println("receive exit signal ", i.String(), ",exit...") exit() os.Exit(0) } } } // NewShutdownSignal new normal Signal channel func NewShutdownSignal() chan os.Signal { c := make(chan os.Signal) // SIGHUP: terminal closed // SIGINT: Ctrl+C // SIGTERM: program exit // SIGQUIT: Ctrl+/ signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT) return c }
http server的例子
以gin框架实现一个http server为例,来演示如何使用上面封装的优雅退出功能:
package main import ( "context" "fmt" "github.com/gin-gonic/gin" "net/http" "os" "os/signal" "syscall" "time" ) // Recover the go routine func Recover(cleanups ...func()) { for _, cleanup := range cleanups { cleanup() } if err := recover(); err != nil { fmt.Println("recover error", err) } } // GoSafe instead go func() func GoSafe(ctx context.Context, fn func(ctx context.Context)) { go func(ctx context.Context) { defer Recover() if fn != nil { fn(ctx) } }(ctx) } func main() { // a gin http server gin.SetMode(gin.ReleaseMode) g := gin.Default() g.GET("/hello", func(context *gin.Context) { // 被 gin 所在 goroutine 捕获 panic("i am panic") }) httpSrv := &http.Server{ Addr: "127.0.0.1:8060", Handler: g, } fmt.Println("server run on:", httpSrv.Addr) go httpSrv.ListenAndServe() // a custom dangerous go routine, 10s later app will crash!!!! GoSafe(context.Background(), func(ctx context.Context) { time.Sleep(time.Second * 10) panic("dangerous") }) // wait until exit signalChan := NewShutdownSignal() WaitExit(signalChan, func() { // your clean code if err := httpSrv.Shutdown(context.Background()); err != nil { fmt.Println(err.Error()) } fmt.Println("http server closed") }) }
运行后立即按Ctrl+C或者在Goland中直接停止:
server run on: 127.0.0.1:8060 ^Creceive exit signal interrupt ,exit... http server closed Process finished with the exit code 0
陷阱和最佳实践
如果你等待10秒后,程序会崩溃,如果是你从C++转过来,你会奇怪为啥没有进入优雅退出环节(` go panic机制和C++ 进程crash,被系统杀死的机制不一样,不会收到系统信号`):
server run on: 127.0.0.1:8060 panic: dangerous goroutine 21 [running]: main.main.func2() /Users/fei.xu/repo/haoshuo/ws-gate/app/test/main.go:77 +0x40 created by main.main /Users/fei.xu/repo/haoshuo/ws-gate/app/test/main.go:75 +0x250 Process finished with the exit code 2
package threading import ( "bytes" "runtime" "strconv" "github.com/zeromicro/go-zero/core/rescue" ) // GoSafe runs the given fn using another goroutine, recovers if fn panics. func GoSafe(fn func()) { go RunSafe(fn) } // RoutineId is only for debug, never use it in production. func RoutineId() uint64 { b := make([]byte, 64) b = b[:runtime.Stack(b, false)] b = bytes.TrimPrefix(b, []byte("goroutine ")) b = b[:bytes.IndexByte(b, ' ')] // if error, just return 0 n, _ := strconv.ParseUint(string(b), 10, 64) return n } // RunSafe runs the given fn, recovers if fn panics. func RunSafe(fn func()) { defer rescue.Recover() fn() }