优雅退出在Golang中的实现
背景
为什么需要优雅关停
- 前台启动。打开终端,在终端中直接启动某个进程,此时终端被阻塞,按CTRL+C退出程序,可以输入其他命令,关闭终端后程序也会跟着退出。
$ ./main
$ # 按CTRL+C退出
- 后台启动。打开终端,以nohup来后台启动某个进程,这样退出终端后,进程仍然会后台运行。
$ nohup main > log.out 2>&1 &
$ ps aux | grep main
# 需要使用 kill 杀死进程
$ kill 8120
实现原理
- 比如上面你在终端中按 `CTRL+C` 后,程序会收到 `SIGINT` 信号。
- 打开的终端被关机,会收到 `SIGHUP` 信号。
- kill 8120 杀死某个进程,会收到 `SIGTERM` 信号。

入门例子
代码
package main import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
) // 优雅退出(退出信号)
func waitElegantExit(signalChan chan os.Signal) {
for i := range c {
switch i {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
// 这里做一些清理操作或者输出相关说明,比如 断开数据库连接
fmt.Println("receive exit signal ", i.String(), ",exit...")
os.Exit(0)
}
}
} func main() {
//
// 你的业务逻辑
//
fmt.Println("server run on: 127.0.0.1:8000") 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) // 阻塞,直到接受到退出信号,才停止进程
waitElegantExit(signalChan)
}
详解
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
实战
封装
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的例子
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")
})
}
server run on: 127.0.0.1:8060
^Creceive exit signal interrupt ,exit...
http server closed Process finished with the exit code 0
陷阱和最佳实践
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
// a custom dangerous go routine, 10s later app will crash!!!!
//go func() {
// time.Sleep(time.Second * 10)
// panic("dangerous")
//}()
// use above code instead!
GoSafe(context.Background(), func(ctx context.Context) {
time.Sleep(time.Second * 10)
panic("dangerous")
})
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()
}
---- The End ----
如有任何想法或者建议,欢迎评论区留言。
——————传说中的分割线——————
大家好,我目前已从C++后端转型为Golang后端,可以订阅关注下《Go和分布式IM》公众号,获取一名转型萌新Gopher的心路成长历程和升级打怪技巧。
优雅退出在Golang中的实现的更多相关文章
- golang中使用Shutdown特性对http服务进行优雅退出使用总结
golang 程序启动一个 http 服务时,若服务被意外终止或中断,会让现有请求连接突然中断,未处理完成的任务也会出现不可预知的错误,这样即会造成服务硬终止:为了解决硬终止问题我们希望服务中断或退出 ...
- golang channel详解和协程优雅退出
非缓冲chan,读写对称 非缓冲channel,要求一端读取,一端写入.channel大小为零,所以读写操作一定要匹配. func main() { nochan := make(chan int) ...
- iota: Golang 中优雅的常量
阅读约 11 分钟 注:该文作者是 Katrina Owen,原文地址是 iota: Elegant Constants in Golang 有些概念有名字,并且有时候我们关注这些名字,甚至(特别)是 ...
- Golang中设置函数默认参数的优雅实现
在Golang中,我们经常碰到要设置一个函数的默认值,或者说我定义了参数值,但是又不想传递值,这个在python或php一类的语言中很好实现,但Golang中好像这种方法又不行.今天在看Grpc源码时 ...
- 优雅处理Golang中的异常
我们在使用Golang时,不可避免会遇到异常情况的处理,与Java.Python等语言不同的是,Go中并没有try...catch...这样的语句块,我们知道在Java中使用try...catch.. ...
- Golang中的自动伸缩和自防御设计
Raygun服务由许多活动组件构成,每个组件用于特定的任务.其中一个模块是用Golang编写的,负责对iOS崩溃报告进行处理.简而言之,它接受本机iOS崩溃报告,查找相关的dSYM文件,并生成开发者可 ...
- 延宕执行,妙用无穷,Go lang1.18入门精炼教程,由白丁入鸿儒,Golang中defer关键字延迟调用机制使用EP17
先行定义,延后执行.不得不佩服Go lang设计者天才的设计,事实上,defer关键字就相当于Python中的try{ ...}except{ ...}finally{...}结构设计中的finall ...
- Node 出现 uncaughtException 之后的优雅退出方案
Node 的异步特性是它最大的魅力,但是在带来便利的同时也带来了不少麻烦和坑,错误捕获就是一个.由于 Node 的异步特性,导致我们无法使用 try/catch 来捕获回调函数中的异常,例如: try ...
- NodeJS服务器退出:完成任务,优雅退出
上一篇文章,我们通过一个简单的例子,学习了NodeJS中对客户端的请求(request)对象的解析和处理,整个文件共享的功能已经完成.但是,纵观整个过程,还有两个地方明显需要改进: 首先,不能共享完毕 ...
随机推荐
- Vue项目中的接口进阶使用
创建services文件夹 1.文件夹apis.index.request的三个文件. 2.apis文件放接口 export const apis = { checkDeviceNo: '/api/c ...
- [2018-03-04] 利用 Settings Sync 插件同步 VS Code 设置
VS Code 已原生支持设置同步,本文仅备份记录 [2018-03-04] 早就听说这个插件了,今天用了一下,确实挺方便的.通过把配置文件创建为 Gist 上来实现了 VS Code 设置的同步,下 ...
- SeataAT模式原理
Seata架构 Seata将分布式事务理解为一个全局事务,它由若干个分支事务组成,一个分支事务就是一个满足ACID的本地事务. Seata架构中有三个角色: TC (Transaction Coord ...
- SpringBoot+Mybatis-Plus整合Sharding-JDBC5.1.1实现单库分表【全网最新】
一.前言 小编最近一直在研究关于分库分表的东西,前几天docker安装了mycat实现了分库分表,但是都在说mycat的bug很多.很多人还是倾向于shardingsphere,其实他是一个全家桶,有 ...
- 服务器安装mysql遇到的坑
服务器安装mysql遇到的坑 一.CentOS7安装MySQL 1.下载:MySQL官方的 Yum Repository wget -i -c http://dev.mysql.com/get/mys ...
- github新项目npm错误
当我们从GitHub或者别人那里拿到项目的时候,一般都是要先npm install 进行安装依赖.但是难免会遇到报错. 出现问题1: 解决方案:清除缓存npm cache clear --force之 ...
- VSCode进一步深入了解学习
紧接上一章节趁热打铁吧,未关注博主的记得关注哦! VSCode设置 (1)关闭预览模式 我们在 VScode 上打开一个新文件的话会覆盖掉以前的文件,这是因为 VSCode 默认开启了预览模式,预览模 ...
- golang的defer踩坑汇总
原文链接:http://www.zhoubotong.site/post/50.html defer语句用于延迟函数调用,每次会把一个函数压入栈中,函数返回前再把延迟的函数取出并执行.延迟函数可以有参 ...
- 有关安装pycocotools的办法
1,首先需要安装Visual C++ 2015构建工具,地址https://download.microsoft.com/download/5/f/7/5f7acaeb-8363-451f-9425- ...
- SpringBoot 集成缓存性能之王 Caffeine
使用缓存的目的就是提高性能,今天码哥带大家实践运用 spring-boot-starter-cache 抽象的缓存组件去集成本地缓存性能之王 Caffeine. 大家需要注意的是:in-memeory ...