go程序不会等待非主goroutine完成就退出;main函数返回即进程终止,所有goroutine被强制中止,不执行defer或清理逻辑,sync.waitgroup是最直接的收尾工具。

Go 程序不会等待非主 goroutine 完成就退出,这是最常踩的坑,也是所有 goroutine 生命周期问题的起点。
main 函数返回即程序终止
goroutine 没有“后台守护”属性。只要 main 函数执行完(哪怕只用了 1 微秒),整个进程立刻退出,所有仍在运行的 goroutine 被强制中止,不触发 defer、不释放 channel、不执行任何清理逻辑。
- 常见错误现象:日志只打了一半、HTTP handler 返回了但响应没写完、文件写入被截断
- 典型误用:
go http.ListenAndServe(...)后直接 return,结果服务瞬间消失 - 根本原因:Go 运行时不维护 goroutine 依赖图,也不做存活引用计数
- 验证方式:在 goroutine 里加
time.Sleep(2 * time.Second),再在 main 末尾加fmt.Println("main done"),你会看到这行输出后程序立即结束,goroutine 的 sleep 被打断
sync.WaitGroup 是最直接的收尾工具
当你明确知道要等几个 goroutine,sync.WaitGroup 是零心智负担的选择。它不涉及 channel 语义或 context 取消,纯粹是计数器。
- 必须在
go语句前调用wg.Add(1),不能在 goroutine 内部 Add —— 否则可能漏加或竞态 - 务必用
defer wg.Done(),确保函数无论从哪个分支 return 都能减计数 - Wait 必须在 main goroutine 中调用,且不能放在 goroutine 启动循环内部(会阻塞后续启动)
- 示例关键片段:
var wg sync.WaitGroup for i := 0; i
channel + select 适合带条件退出的长期 goroutine
当 goroutine 需要持续运行(比如监听消息、轮询状态),靠 WaitGroup 就不合适了 —— 它没有“提前叫停”的能力。这时要用 channel 通信配合 select。
- 不要用无缓冲 channel 直接接收:如果 sender 没发,goroutine 会永久阻塞,变成 goroutine leak
- 推荐模式:传入一个
done chan struct{},在select中监听它,收到就 return - 关闭
done是安全的退出信号,select会立即响应,且多次关闭无 panic - 避免在 goroutine 内部 close(done),应由外部控制方 close,否则可能 race
- 示例骨架:
func worker(done chan struct{}) { for { select { case
context.WithCancel 是带传播能力的退出方案
当 goroutine 之间存在父子关系(比如 HTTP handler 启动子任务),需要统一取消整条链路时,context 比裸 channel 更健壮。
- 不要把
context.Background()直接传给长期 goroutine —— 它永远不会被 cancel - 必须用
ctx, cancel := context.WithCancel(parentCtx)创建可取消子 ctx,并在适当时机调用cancel() - goroutine 内通过
select { case 响应取消,同时可读取 <code>ctx.Err()区分是 canceled 还是 timeout - 注意:
cancel()调用后,ctx.Done()才会关闭;未调用则永远不关闭,别指望它自动超时 - 容易忽略的点:父 context 被 cancel 后,所有派生出的子 context 也会同步关闭,无需手动 cancel 每一个
真正难的不是启动 goroutine,而是判断它该什么时候停、由谁来通知它停、以及停之前有没有数据要 flush 或连接要关闭 —— 这些细节藏在业务逻辑里,而不是 runtime 文档里。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











