main函数中不能用os.exit()触发清理,因其跳过defer导致资源泄漏;应监听sigint和sigterm信号,用带超时的context调用http.server.shutdown(),所有goroutine需统一响应同一退出信号源。

main 函数里不能用 os.Exit() 触发清理逻辑
这是最常踩的坑:很多人在信号 handler 里直接调用 os.Exit(0),以为能“干净退出”,结果所有 defer 全被跳过,数据库连接没关、临时文件没删、后台 goroutine 残留——进程是没了,但资源还在泄漏。
os.Exit() 会立即终止进程,不走函数返回路径,所以 main 函数里的 defer 不执行,goroutine 里的 defer 更是完全无效。真正可靠的清理,必须主动触发、同步等待、显式释放。
- 所有关键资源(DB 连接、文件句柄、HTTP server listener)都要在收到信号后手动关闭
- 长期运行的 goroutine 必须响应统一退出信号(比如
ctx.Done()或done chan struct{}),不能靠“等主函数结束”来隐式退出 - 避免在 signal handler goroutine 里做耗时操作(如写日志、关 DB),只负责通知,清理逻辑放主线程或专用 shutdown 流程里
signal.Notify() 必须同时监听 syscall.SIGINT 和 syscall.SIGTERM
只监听 os.Interrupt(即 Ctrl+C)是常见错误。容器环境(Docker/K8s)、systemd 服务、kill 命令默认发的是 SIGTERM,不是 SIGINT。漏掉它,程序在生产环境就无法优雅终止。
正确写法是:
sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
注意:syscall.SIGKILL(kill -9)永远不要加进去——它不可捕获,加了也没用,还可能误导自己。
- channel 缓冲大小设为 1 足够,避免多个信号堆积导致重复 shutdown
- 不要用
signal.Ignore()或signal.Stop()在 shutdown 阶段手动取消监听,容易引发竞态;收到第一个信号后直接消费并退出即可 - 如果程序还要支持
SIGHUP(比如热重载配置),可以额外加上,但别和 shutdown 逻辑混在一起
http.Server.Shutdown() 必须传带超时的 context.Context
srv.Shutdown(context.Background()) 是典型错误——它等价于“立刻关”,不给正在处理的请求留时间,连接会被重置,上传中断,日志截断。
必须用带超时的子 context,并确保超时时间大于业务中最长请求耗时(比如文件上传要 30 秒,那就设 45 秒):
ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil && err != http.ErrServerClosed {
log.Printf("shutdown error: %v", err)
}
还要记得忽略 http.ErrServerClosed:这是 Shutdown() 正常完成时 ListenAndServe() 返回的错误,不是异常。
- 不要在
Shutdown()之后再调srv.Close(),会 panic - 如果 HTTP server 启动失败(比如端口被占),
ListenAndServe()会直接返回错误,此时Shutdown()不应被调用 - 超时 context 的 cancel() 一定要 defer,否则可能泄漏 goroutine
后台 goroutine 必须统一响应同一个 done 或 ctx
HTTP server 关了,不代表整个程序就能安全退出——数据库轮询、定时任务、消息消费者这些 goroutine 如果还在跑,轻则资源泄漏,重则写脏数据。
所有长期 goroutine 都得监听同一个退出信号源,推荐用 context.Context:
go func() {
ticker := time.NewTicker(10 * time.Second)
defer ticker.Stop()
for {
select {
case <p>收到信号后,先 <code>cancel()</code>,再 <code>Shutdown()</code>,最后用 <code>sync.WaitGroup</code> 或 <code>time.AfterFunc()</code> 等待它们自然退出。别用 <code>time.Sleep()</code> 硬等——不确定是否真结束了,也不够精确。</p>
- goroutine 内部不能只用
if ctx.Err() != nil判断一次就退出,必须用select持续监听ctx.Done() - 清理动作(如
db.Close()、redis.Unsubscribe())要放在case 分支里,确保只执行一次 - 如果用了
sync.WaitGroup,记得每个 goroutine 开头wg.Add(1),结尾wg.Done(),且wg.Wait()必须在所有清理之后、os.Exit()之前
真正的难点不在代码行数,而在于所有 goroutine 是否真正“听指挥”——一个没响应的后台任务,就足以让整个优雅退出失效。信号监听只是起点,资源回收的边界必须清晰定义:哪些该关、谁来关、按什么顺序关、超时怎么判,缺一不可。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











