17370845950

如何在 Go 程序中优雅响应 Ctrl+C(而非 Ctrl+D)进行资源清理

go 程序无法直接捕获 ctrl+d(eof),但可通过监听 `os.interrupt`(对应 ctrl+c)实现优雅退出,在终止前执行 ec2 清理等关键收尾逻辑。

在 Go 中,Ctrl + D 并非信号事件,而是终端输入流的 EOF(End-of-File)标志,通常影响 os.Stdin.Read() 等读取操作,不会向进程发送系统信号,因此无法“全局捕获”或用于触发中断处理逻辑。真正可用于优雅终止并执行清理操作的是 Ctrl + C —— 它会向进程发送 SIGINT 信号,Go 可通过 os/signal 包可靠监听并响应。

以下是标准、健壮的优雅退出模式:

package main

import (
    "context"
    "fmt"
    "os"
    "os/signal"
    "syscall"
    "time"
)

func cleanupEC2() error {
    fmt.Println("⏳ Cleaning up EC2 resources...")
    // 示例:调用 AWS SDK 终止实例、释放弹性 IP、删除安全组等
    // e.g., ec2Client.TerminateInstances(...)
    time.Sleep(1 * time.Second) // 模拟清理耗时
    fmt.Println("✅ EC2 cleanup completed.")
    return nil
}

func main() {
    // 启动主业务逻辑(如轮询、HTTP 服务、长期任务)
    fmt.Println("? Application started. Press Ctrl+C to exit gracefully.")

    // 设置信号监听器
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM) // 同时支持 Ctrl+C 和 kill -15

    // 启动一个 goroutine 处理信号
    go func() {
        sig := <-sigChan
        fmt.Printf("\n⚠️  Received signal: %s. Initiating graceful shutdown...\n", sig)

        // 执行清理逻辑(阻塞式,确保完成)
        if err := cleanupEC2(); err != nil {
            fmt.Printf("❌ Cleanup failed: %v\n", err)
        }

        fmt.Println("? Goodbye.")
        os.Exit(0)
    }()

    // 模拟长期运行的任务(例如监控循环或 HTTP server)
    select {} // 阻塞主 goroutine,等待信号
}

关键要点说明:

  • 使用 signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM) 监听标准中断信号;
  • SIGINT 对应 Ctrl+C(推荐方式),SIGTERM 支持 kill -15 等外部终止请求;
  • 不要依赖 Ctrl+D 做退出控制——它仅关闭 stdin,对后台程序无意义,且不可靠(例如 go run 启动的程序通常不读 stdin);
  • 清理逻辑应在信号处理 goroutine 中同步执行,避免竞态;若需超时保护,可结合 context.WithTimeout;
  • os.Exit(0) 应在清理完成后显式调用,确保 defer 不再执行(因其已在主 goroutine 退出后失效)。

? 额外建议:
若程序本身需从 stdin 读取命令(如交互式 CLI),可同时监听 io.EOF(来自 Ctrl+D)作为用户主动退出输入流的提示,但此时仍应配合 Ctrl+C 信号机制保障资源安全释放——二者职责不同:前者是“输入结束”,后者才是“进程终止”。

通过该模式,你可在 EC2 创建中途被手动中断时,确保云资源不被遗忘,显著提升生产脚本的健壮性与运维安全性。