在Go程序运行场景中,用户可能通过Ctrl+C主动终止进程,或者通过Ctrl+D在终端输入场景下标记输入结束,如果程序没有相应的响应逻辑,很容易出现文件未关闭、连接未释放、临时数据未保存等问题,因此需要针对性设计信号捕获和清理机制。

信号类型与场景区分
首先需要明确两个触发行为的本质区别:
- Ctrl+C:对应的是Unix系统中的
SIGINT信号,属于进程级别的中断信号,操作系统会将该信号发送给当前前台进程组。 - Ctrl+D:并不是真正的信号,而是终端输入流结束的标记,当程序从标准输入读取数据时,遇到这个标记会认为输入已经结束,
io.Reader会返回io.EOF错误。
响应Ctrl+C的实现方案
Go语言标准库的os/signal包提供了信号监听能力,我们可以通过监听SIGINT信号来捕获Ctrl+C操作,同时结合context实现优雅退出。
基础监听示例
下面的代码展示了如何监听SIGINT信号,在收到信号后执行清理逻辑再退出:
package main
import (
"context"
"fmt"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
// 创建可取消的上下文
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 创建信号接收通道,监听SIGINT信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
// 启动业务协程
go businessTask(ctx)
// 阻塞等待信号
select {
case sig := <-sigChan:
fmt.Printf("收到信号:%v,开始执行清理逻辑n", sig)
// 取消上下文,通知业务协程停止
cancel()
// 等待业务协程完成清理,设置超时防止无限等待
time.Sleep(2 * time.Second)
// 执行全局清理逻辑
cleanup()
fmt.Println("程序退出完成")
}
}
// 模拟业务任务
func businessTask(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("业务任务收到停止信号,开始清理内部资源")
return
default:
fmt.Println("业务任务运行中...")
time.Sleep(1 * time.Second)
}
}
}
// 全局清理逻辑
func cleanup() {
// 这里可以放关闭文件、断开数据库连接、保存临时数据等逻辑
fmt.Println("执行全局资源释放:关闭数据库连接、保存临时文件")
}
响应Ctrl+D的实现方案
Ctrl+D的场景通常出现在程序需要从标准输入读取内容的场景,比如命令行交互工具、读取管道输入的程序,此时只需要处理io.EOF错误即可。
读取标准输入示例
下面的代码展示了如何读取标准输入,在用户按下Ctrl+D时执行清理逻辑:
package main
import (
"bufio"
"fmt"
"io"
"os"
"time"
)
func main() {
fmt.Println("请输入内容,按Ctrl+D结束输入:")
reader := bufio.NewReader(os.Stdin)
for {
line, err := reader.ReadString('n')
if err != nil {
if err == io.EOF {
fmt.Println("检测到输入结束(Ctrl+D),开始执行清理逻辑")
cleanup()
fmt.Println("程序退出完成")
return
}
fmt.Printf("读取输入出错:%vn", err)
return
}
fmt.Printf("你输入的内容是:%s", line)
}
}
// 清理逻辑
func cleanup() {
// 处理输入结束后的资源释放
fmt.Println("保存输入内容到文件、关闭相关资源")
time.Sleep(1 * time.Second)
}
同时响应两种场景的完整示例
如果程序既需要处理Ctrl+C中断,又需要处理Ctrl+D输入结束,可以将两种逻辑结合起来:
package main
import (
"bufio"
"context"
"fmt"
"io"
"os"
"os/signal"
"syscall"
"time"
)
func main() {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 监听Ctrl+C信号
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT)
// 启动输入处理协程
inputDone := make(chan struct{})
go handleInput(ctx, inputDone)
// 等待两种退出场景
select {
case sig := <-sigChan:
fmt.Printf("收到信号:%v,执行退出逻辑n", sig)
cancel()
// 等待输入协程结束
<-inputDone
cleanup()
fmt.Println("程序通过Ctrl+C退出完成")
case <-inputDone:
// 输入结束(Ctrl+D)触发
cancel()
cleanup()
fmt.Println("程序通过Ctrl+D退出完成")
}
}
// 处理标准输入
func handleInput(ctx context.Context, done chan struct{}) {
defer close(done)
reader := bufio.NewReader(os.Stdin)
fmt.Println("请输入内容,按Ctrl+D结束,或按Ctrl+C中断程序:")
for {
select {
case <-ctx.Done():
fmt.Println("输入处理协程收到停止信号")
return
default:
line, err := reader.ReadString('n')
if err != nil {
if err == io.EOF {
fmt.Println("检测到Ctrl+D,输入结束")
return
}
fmt.Printf("输入读取错误:%vn", err)
return
}
fmt.Printf("输入内容:%s", line)
}
}
}
// 统一清理逻辑
func cleanup() {
fmt.Println("执行统一清理:关闭资源、保存状态")
time.Sleep(1 * time.Second)
}
注意事项
- 信号监听通道建议设置缓冲区,避免信号发送时因为没有接收者而丢失。
- 清理逻辑需要设置超时机制,避免某个清理步骤卡住导致程序无法退出。
- 如果程序使用了第三方信号处理库,需要注意是否会和
os/signal的监听逻辑冲突。 - Windows系统下Ctrl+C的行为和Unix类系统一致,但Ctrl+D在Windows终端的对应操作是Ctrl+Z加回车,不过
io.EOF的处理逻辑是通用的。