go程序需优雅退出:用sync.waitgroup跟踪goroutine,配合context.withtimeout响应信号;http server用shutdown而非close;数据库等资源须显式close并设超时。

main函数退出后goroutine直接被杀,怎么等它们自然结束
Go程序里main函数一返回,整个进程就退出,所有还在跑的goroutine会被强制终止——不管它是不是正在写文件、发HTTP请求、还是关数据库连接。这不是“优雅退出”,是“拔电源”。关键不是阻止退出,而是让main多等一会儿,直到后台任务自己收尾。
最常用也最可靠的方式是用sync.WaitGroup显式跟踪活跃goroutine:
-
WaitGroup.Add(1)在启动goroutine前调用(注意别在goroutine里加,容易漏) - goroutine末尾必须调
WaitGroup.Done(),最好包在defer里 -
main函数末尾调WaitGroup.Wait(),这会阻塞直到计数归零
别用time.Sleep硬等——你永远猜不准要睡多久,超时了丢数据,睡太长又拖慢退出。
想响应系统信号(比如Ctrl+C)做 graceful shutdown
只等WaitGroup还不够:用户按Ctrl+C或容器发SIGTERM时,程序得立刻开始清理,而不是傻等所有goroutine自己结束。这时候得引入os.Signal和context.Context配合。
立即学习“go语言免费学习笔记(深入)”;
核心逻辑是:收到信号后,通知所有长期运行的goroutine该停了,并给它们留出合理时间收尾:
- 用
signal.Notify监听os.Interrupt和syscall.SIGTERM - 创建带超时的
context.WithTimeout,超时时间建议设为5–10秒(取决于业务) - 把
ctx传给每个需要响应退出的goroutine,它们内部定期检查ctx.Err() != nil -
main中先发信号再WaitGroup.Wait(),避免死等
注意:context.WithCancel本身不带超时,如果goroutine卡死没响应,程序会永远hang住;必须用WithTimeout兜底。
HTTP server关闭时连接被粗暴中断怎么办
直接调http.Server.Close()会立刻断开所有未完成的请求,客户端可能收不到响应。Go 1.8+ 提供了Shutdown方法,但它依赖你传入一个context.Context来控制等待窗口。
常见错误是把Shutdown放在WaitGroup.Wait()之后——这时server早被关了,Shutdown根本没机会执行。正确顺序是:
- 收到退出信号后,立即调
server.Shutdown(ctx)(传入带超时的ctx) -
Shutdown会尝试优雅关闭:不再接受新连接,但允许已有请求走完 - 然后才调
WaitGroup.Wait()等其他goroutine(比如定时任务、消息消费者)结束
如果Shutdown返回context.DeadlineExceeded,说明有请求卡太久,此时应记录日志,但不要panic——进程仍会退出,只是部分请求丢了响应。
数据库连接池、第三方client没关干净导致进程卡住
很多库(比如sql.DB、redis.Client、http.Client)内部用了后台goroutine维持连接或做健康检查。如果你只关了server、等了业务goroutine,但忘了调它们的Close()方法,这些后台goroutine可能还在跑,WaitGroup永远等不到归零。
每个资源都要显式释放:
-
sql.DB.Close()—— 不仅释放连接,还停止内部的连接回收goroutine -
redis.Client.Close()—— 否则它的连接管理器会一直重连 -
http.Client如果用了自定义Transport,记得transport.CloseIdleConnections()
最容易被忽略的是:这些Close()调用本身可能阻塞(比如DB在等事务提交),所以建议也包进超时context里,或者至少加个defer确保执行。没有Close()的第三方库,得查文档看它是否支持优雅退出,不行就只能硬等或改用别的库。
真正麻烦的从来不是“怎么等”,而是“你到底在等哪些东西”——漏掉任何一个没关的资源,main就会卡住,或者更糟:看起来退出了,其实后台还在偷偷跑着。










