Golang如何优雅退出
前言
在Go中,信号(Signals)处理是一项基础而又重要的技能,它关乎着程序如何响应外部事件,特别是如何优雅地终止进程。本文带你探讨Go程序中的信号处理机制。以及见过写的比较好的Demo。
信号基础
信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方式。一个信号就是一个异步的通知,发送给某个进程,或者同进程的某个线程,告诉它们某个事件发生了。
当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。
信号作为Unix/Linux系统中用于进程间通信的一种机制,它允许操作系统通知进程发生了某种事件。在Go中,信号通过os/signal
包进行处理,该包提供了接收和处理信号的功能。
常见信号
日常开发中常见信号有:
- SIGINT:用户按下
Ctrl+C
时发送,通常用来中断进程。 - SIGTERM:默认的进程终止信号,用于请求进程正常退出。
- SIGKILL:不能被捕获或忽略,直接终止进程。
- SIGHUP:挂起信号,通常意味着终端连接断开。
Go信号处理流程
- 注册信号处理器:使用
signal.Notify
函数注册一个或多个信号的处理函数。 - 等待信号:通过
signal.NotifyContext
或自建循环等待信号到来。 - 执行清理操作:在信号处理函数中执行资源释放、保存状态等操作。
- 优雅退出:完成清理后,正常结束程序。
Go信号使用方法
函数声明为:
func Notify(c chan<- os.Signal, sig ...os.Signal)
官方描述:
Notify
函数让signal
包将输入信号转发到c。 如果没有列出要传递的信号,会将所有输入信号传递到c
;否则只传递列出的输入信号。
signal
包不会为了向c发送信息而阻塞(就是说如果发送时c阻塞了,signal
包会直接放弃):调用者应该保证c有足够的缓存空间可以跟上期望的信号频率。对使用单一信号用于通知的通道,缓存为1就足够了。
示例代码:
方法1:
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
//创建监听退出chan
c := make(chan os.Signal)
//监听指定信号 ctrl+c kill
signal.Notify(c, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM,
syscall.SIGQUIT, syscall.SIGUSR1, syscall.SIGUSR2)
go func() {
for s := range c {
switch s {
case syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM, syscall.SIGQUIT:
fmt.Println("Program Exit...", s)
GracefullExit()
case syscall.SIGUSR1:
fmt.Println("usr1 signal", s)
case syscall.SIGUSR2:
fmt.Println("usr2 signal", s)
default:
fmt.Println("other signal", s)
}
}
}()
// 具体业务逻辑代码
fmt.Println("Program Start...")
sum := 0
for {
sum++
fmt.Println("sum:", sum)
time.Sleep(time.Second)
}
}
func GracefullExit() {
fmt.Println("Start Exit...")
fmt.Println("Execute Clean...")
fmt.Println("End Exit...")
os.Exit(0)
}
方法2:
func main() {
// shutdown signal
done := make(chan bool, 1)
// listen for interrupt signal to gracefully shutdown the application
go func() {
sigch := make(chan os.Signal, 1)
signal.Notify(sigch, os.Interrupt, syscall.SIGTERM)
<-sigch
done <- true
}()
// 业务逻辑执行的地方
go func() {
if err := service.Run(); err != nil {
// 处理服务异常退出的逻辑
}
done <- true
}()
<-done
// 程序退出时的逻辑处理
doClean()
}
方法3:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 创建上下文用于监听信号
ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer stop()
// 定义一个goroutine模拟清理工作
go func() {
<-ctx.Done()
fmt.Println("开始清理工作...")
time.Sleep(2 * time.Second) // 模拟清理过程
fmt.Println("清理完成,准备退出。")
}()
fmt.Println("程序正在运行,按Ctrl+C或发送SIGTERM信号退出。")
// 主goroutine等待信号
<-ctx.Done()
fmt.Println("接收到信号,即将退出。")
}
所有信号类型
信号 | 值 | 动作 | 说明 |
---|---|---|---|
SIGHUP | 1 | Term | 终端控制进程结束(终端连接断开) |
SIGINT | 2 | Term | 用户发送INTR字符(Ctrl+C)触发 |
SIGQUIT | 3 | Core | 用户发送QUIT字符(Ctrl+/)触发 |
SIGILL | 4 | Core | 非法指令(程序错误、试图执行数据段、栈溢出等) |
SIGABRT | 6 | Core | 调用abort函数触发 |
SIGFPE | 8 | Core | 算术运行错误(浮点运算错误、除数为零等) |
SIGKILL | 9 | Term | 无条件结束程序(不能被捕获、阻塞或忽略) |
SIGSEGV | 11 | Core | 无效内存引用(试图访问不属于自己的内存空间、对只读内存空间进行写操作) |
SIGPIPE | 13 | Term | 消息管道损坏(FIFO/Socket通信时,管道未打开而进行写操作) |
SIGALRM | 14 | Term | 时钟定时信号 |
SIGTERM | 15 | Term | 结束程序(可以被捕获、阻塞或忽略) |
SIGUSR1 | 30,10,16 | Term | 用户保留 |
SIGUSR2 | 31,12,17 | Term | 用户保留 |
SIGCHLD | 20,17,18 | Ign | 子进程结束(由父进程接收) |
SIGCONT | 19,18,25 | Cont | 继续执行已经停止的进程(不能被阻塞) |
SIGSTOP | 17,19,23 | Stop | 停止进程(不能被捕获、阻塞或忽略) |
SIGTSTP | 18,20,24 | Stop | 停止进程(可以被捕获、阻塞或忽略) |
SIGTTIN | 21,21,26 | Stop | 后台程序从终端中读取数据时触发 |
SIGTTOU | 22,22,27 | Stop | 后台程序向终端中写数据时触发 |
总结
信号处理是Go程序设计中的重要一环,它不仅关系到程序的健壮性,还直接影响用户体验。通过合理设计信号处理逻辑,可以确保程序能够优雅地响应外部信号,及时释放资源,避免数据丢失或服务异常。记住,信号处理应当简洁高效,避免阻塞和重复处理,同时利用Go的并发特性来优化清理流程,以实现真正的“优雅退出”。