go 程序中启动的 goroutine 未输出预期结果,往往并非代码错误,而是因主 goroutine 过早退出导致整个程序终止——本文详解其原理、复现场景及三种专业级同步方案(waitgroup、channel、time.sleep 的适用边界)。
在 Go 中,go say(“world”) 启动了一个新的 goroutine,但程序的生命周期由 main goroutine 决定:一旦 main() 函数执行完毕并返回,整个进程立即退出,所有尚未完成的 goroutine(包括正在运行或排队中的)都会被强制终止。这正是原始示例中只看到 “hello” 输出而完全缺失 “world” 的根本原因:
func main() {
go say("world") // 新 goroutine 已启动,但无任何等待机制
say("hello") // main goroutine 执行完这行后立即退出 → 程序终结!
}
尽管 goroutine 已调度,但 say(“hello”) 的 5 次打印极快完成,main() 函数随即结束,操作系统回收进程资源,say(“world”) 甚至可能还未被调度执行,更遑论完成全部循环。
✅ 正确做法:显式同步,确保主 goroutine 等待子任务完成
方案一:使用 sync.WaitGroup(推荐,最常用且语义清晰)
WaitGroup 是专为“等待一组 goroutine 完成”设计的标准工具,适用于已知并发数量的协作场景:
package main
import (
"fmt"
"sync"
)
func say(s string) {
for i := 0; i < 5; i++ {
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup
// 增加计数器:1 表示等待 say("world")
wg.Add(1)
go func() {
defer wg.Done() // 确保完成时计数器减 1
say("world")
}()
// 主 goroutine 同步执行 say("hello")
say("hello")
// 阻塞等待所有 goroutine 完成
wg.Wait()
}
⚠️ 注意:wg.Done() 必须在 goroutine 内部调用(通常用 defer 保证),且 wg.Add() 必须在 go 语句前执行,否则存在竞态风险。
方案二:使用 Channel(适合需要传递结果或复杂协调的场景)
Channel 更适合需要数据通信、条件等待或动态 goroutine 生命周期管理的场景。以下为简化版双 goroutine 协同示例: