Golang如何优雅退出

前言

在Go中,信号(Signals)处理是一项基础而又重要的技能,它关乎着程序如何响应外部事件,特别是如何优雅地终止进程。本文带你探讨Go程序中的信号处理机制。以及见过写的比较好的Demo。

信号基础

信号(Signal)是Linux, 类Unix和其它POSIX兼容的操作系统中用来进程间通讯的一种方式。一个信号就是一个异步的通知,发送给某个进程,或者同进程的某个线程,告诉它们某个事件发生了。

当信号发送到某个进程中时,操作系统会中断该进程的正常流程,并进入相应的信号处理函数执行操作,完成后再回到中断的地方继续执行。

信号作为Unix/Linux系统中用于进程间通信的一种机制,它允许操作系统通知进程发生了某种事件。在Go中,信号通过os/signal包进行处理,该包提供了接收和处理信号的功能。

常见信号

日常开发中常见信号有:

  • SIGINT:用户按下Ctrl+C时发送,通常用来中断进程。
  • SIGTERM:默认的进程终止信号,用于请求进程正常退出。
  • SIGKILL:不能被捕获或忽略,直接终止进程。
  • SIGHUP:挂起信号,通常意味着终端连接断开。

Go信号处理流程

  1. 注册信号处理器:使用signal.Notify函数注册一个或多个信号的处理函数。
  2. 等待信号:通过signal.NotifyContext或自建循环等待信号到来。
  3. 执行清理操作:在信号处理函数中执行资源释放、保存状态等操作。
  4. 优雅退出:完成清理后,正常结束程序。

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的并发特性来优化清理流程,以实现真正的“优雅退出”。

关于我
loading