go 中 main 函数退出会导致所有 goroutine 被强制终止,这是设计原则而非 bug;select{} 是最轻量的阻塞方式,因它不消耗 cpu 且永久挂起主协程,而 for{} 空循环会持续占用 cpu,time.sleep 则不可靠、难维护。

main 函数提前退出,协程就被强制杀掉——这不是 bug,是 Go 的设计原则。你写的 go doWork() 不会自动让 main 等它,除非你显式干预。
为什么 select{} 是最轻量的阻塞方式
很多人用 for{} 或 time.Sleep(1 卡住 <code>main,但这些要么空转占 CPU,要么只是“赌时间够长”。select{} 没有任何 case,Go 运行时会把它识别为**永久阻塞且零开销**的操作。
- 它不消耗 CPU,不调度 goroutine,纯粹挂起当前 goroutine
- 不能被 signal 中断(这点和 channel 阻塞不同),适合调试或简单服务
- 写法极简:
select{}放在main最后一行即可
示例:
func main() {
go func() {
time.Sleep(2 * time.Second)
fmt.Println("done")
}()
select{} // 主协程停在这,不会退出
}
用 chan struct{} 配合信号实现可中断阻塞
生产环境不能靠 select{} 硬挂——你没法优雅退出。真正要用的是带退出信号的通道阻塞,核心是监听 os.Signal 并写入一个 quit 通道。
- 必须用
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM),否则 Ctrl+C 无效 -
quit通道要带缓冲(make(chan os.Signal, 1)),避免信号丢失 - 阻塞写法是
,不是 <code>quit (后者是发信号,前者是收)
示例:
func main() {
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
go runBackgroundTask()
<h3>
<code>sync.WaitGroup</code> 适用于明确生命周期的任务</h3><p>如果你启动的是一批“有始有终”的 goroutine(比如批量处理、一次性任务),<code>sync.WaitGroup</code> 比通道更语义清晰:它表达的是“等全部完成”,而不是“等某个事件”。</p>
-
wg.Add(1)必须在go之前调用,否则可能 race -
defer wg.Done()要放在 goroutine 内部最开头,确保无论怎么 return 都能计数减一 -
wg.Wait()在main里调用,会阻塞直到所有Done()被触发
注意:它不适合长期运行的服务(比如 HTTP server、定时器),因为那些 goroutine 永远不会调用 Done()。
别踩这些坑
常见错误不是语法问题,而是对 Go 并发模型的理解偏差:
- 在 goroutine 里用
time.Sleep模拟工作,却忘了main已经退出——这说明你没加任何同步机制 - 把
写成 <code>quit ,结果主协程根本没阻塞,直接退出 - 用
context.WithCancel但没把 ctx 传进 goroutine,导致 cancel 无法传播 - 以为
runtime.Goexit()能阻止main退出——它只退出当前 goroutine,对main无效
真正关键的不是“怎么卡住”,而是“卡住之后怎么响应退出”。select{} 是起点,signal + channel 是常态,WaitGroup 是特例——选哪个,取决于你的 goroutine 是“长期驻留”还是“限期执行”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











