Go程序通过os.Signal和signal.Notify捕获SIGINT、SIGTERM等信号实现优雅启停;启动逻辑应封装为显式函数并在main中调用,避免硬编码或使用init;shutdown需用defer注册,确保资源清理。

Go 程序如何监听系统信号实现优雅启停
Go 本身没有“系统事件钩子”概念,但通过 os.Signal 和 signal.Notify 可以精准捕获 SIGINT(Ctrl+C)、SIGTERM(kill 默认信号)等关键系统信号,这是实现启动初始化与关闭清理的通用且可靠方式。
启动逻辑该放哪?别在 main() 开头硬编码
启动逻辑(如加载配置、连接数据库、注册服务)应封装为独立函数,而非堆在 main() 开头。否则一旦关闭流程触发,无法保证它已执行完毕,容易导致清理时访问未初始化资源。
- 用
init()不合适:它无返回值、不能报错、无法等待异步依赖就绪 - 推荐模式:
main()中调用显式启动函数,检查错误并提前退出 - 示例:
func main() { if err := startup(); err != nil { log.Fatal(err) } defer shutdown() // 注意:defer 在 main return 或 panic 时才执行 signal.Notify(<code>sigChan</code>, syscall.SIGINT, syscall.SIGTERM) <-sigChan }
shutdown() 里必须阻塞等待清理完成,不能只发 goroutine
常见错误是把关闭逻辑扔进 goroutine 后直接 return —— 主 goroutine 一退出,整个进程就终止,goroutine 被强制杀死,数据库连接没关闭、文件没 flush、HTTP server 没 graceful shutdown。
Go 配置库,使用 spf13/viper — 分层优先级(flag > env >file > KV > default),提供 BindPFlag/BindPFlags、SetEnvPrefix + SetEnvKeyReplace 等功能。
-
http.Server.Shutdown()必须传入 context 并等待其完成,不能只调srv.Close() - 数据库连接池建议用
db.Close(),它会阻塞直到所有连接归还 - 自定义资源(如文件句柄、协程池)需设计同步机制,比如用
sync.WaitGroup或chan struct{}等待全部 worker 退出 - 示例片段:
func shutdown() { log.Println("shutting down...") ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() if err := httpServer.Shutdown(ctx); err != nil { log.Printf("HTTP shutdown error: %v", err) } if err := db.Close(); err != nil { log.Printf("DB close error: %v", err) } }
为什么 SIGQUIT 和 SIGKILL 通常不该处理
SIGQUIT(Ctrl+\)默认触发 core dump;SIGKILL(kill -9)根本不能被 Go 程序捕获 —— 这是操作系统强制终止,任何 defer、signal handler、甚至 runtime 都来不及响应。
立即学习“go语言免费学习笔记(深入)”;
- 试图监听
SIGKILL会静默失败,signal.Notify不报错也不生效 - 处理
SIGQUIT容易干扰调试行为(如本意是让程序崩溃留 core),一般无需干预 - 真正要关注的只有
SIGINT(开发调试)和SIGTERM(生产部署,如 systemd、k8s termination) - 若需支持强制立即退出,应由业务逻辑提供独立的 admin endpoint 或信号通道,而非依赖系统信号
最易被忽略的是 shutdown 的超时控制和上下文传递 —— 没有 timeout 的 Shutdown() 可能永远卡住,而 context 未传到底层依赖(如 grpc.ClientConn、sql.DB)会导致部分资源无法释放。

















