
Go 程序中若在主 goroutine 使用空 for{} 循环,会导致调度器无法切换到其他 goroutine(如异步发送邮件的协程),从而造成看似“卡死”的假象;应改用 select{} 实现无 CPU 消耗的永久等待。
go 程序中若在主 goroutine 使用空 `for{}` 循环,会导致调度器无法切换到其他 goroutine(如异步发送邮件的协程),从而造成看似“卡死”的假象;应改用 `select{}` 实现无 cpu 消耗的永久等待。
在 Go 并发编程中,for {} 看似是让主程序“保持运行”的简单方式,但其本质是一个忙等待(busy loop):它持续占用当前 OS 线程,且因无函数调用、栈检查或内存分配,Go 调度器无法插入调度点(preemption point),导致其他 goroutine(包括你启动的 go SendEmail(...))永远得不到执行机会——即使 SendEmail 内部调用了阻塞的网络 I/O(如 SMTP 连接),也无法被调度启动。
这正是你观察到“单个 goroutine 不发送、加一个同步调用后反而两个都成功”的原因:SendEmail("TEST") 是同步执行,其内部 smtp.SendMail 会触发系统调用(如 connect、write),此时调度器获得介入时机,顺带调度了先前挂起的 goroutine;而纯 for{} 则彻底剥夺了调度权。
✅ 正确做法:用 select{} 替代 for{}
select{} 是 Go 的原生阻塞原语,它会让当前 goroutine 进入休眠状态,主动交出 OS 线程,不消耗 CPU,同时允许调度器自由调度其他就绪的 goroutine:
func main() {
go SendEmail("TEST")
select {} // ✅ 永久等待,零 CPU 占用,goroutine 可被正常调度
}
⚠️ 注意事项与最佳实践:
- 永远避免 for{} 空循环:它不仅阻碍调度,还会 100% 占用一个 CPU 核心,违背 Go “轻量并发”设计哲学。
- GOMAXPROCS 影响有限:虽然 Go 1.5+ 默认 GOMAXPROCS = numCPU,多线程下理论上可能缓解(OS 级线程切换),但不能依赖此行为——调度公平性与可预测性必须由代码显式保证。
- 真实场景应使用信号控制:生产环境不应无限等待,推荐通过 sync.WaitGroup 或 context.Context + channel 实现优雅退出:
func main() {
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
SendEmail("TEST")
}()
wg.Wait() // 等待邮件发送完成,更可控、可测试
}
- SMTP 安全提醒:示例中硬编码邮箱密码存在严重安全隐患,务必改用环境变量或密钥管理服务;Gmail 需开启“应用专用密码”或使用 OAuth2,且 smtp.PlainAuth 在 TLS 下才安全(建议显式配置 smtp.StartTLS)。
总结:Go 的并发模型建立在协作式调度之上,for{} 破坏了这一前提;select{} 是标准、高效、符合 Go idioms 的等待方式。理解调度机制,才能写出真正健壮的并发程序。











