
Go 程序中,goroutine 是并发执行的轻量级线程,但其运行依赖于 main 函数的持续存在;一旦 main 函数返回,整个程序立即终止,未完成的 goroutine 将被强制丢弃,不会执行或部分执行。
go 程序中,goroutine 是并发执行的轻量级线程,但其运行依赖于 `main` 函数的持续存在;一旦 `main` 函数返回,整个程序立即终止,未完成的 goroutine 将被强制丢弃,不会执行或部分执行。
你观察到的现象——调用 say("world") 后启动 go say("hello") 却完全看不到 "hello" 输出,或两个 goroutine 同时启动却毫无输出——根本原因并非调度失败或语法错误,而是 main 函数在 goroutine 开始执行前就已退出。
Go 的程序执行模型明确规定:当 main 函数执行完毕(即 main() 返回),整个程序立即结束,所有正在运行或尚未调度的 goroutine 会被无条件终止。这与操作系统线程不同,goroutine 没有“后台守护”语义。
为什么这些情况表现不同?
- ✅ go say("world"); say("hello"):say("hello") 是同步调用,在 main 中阻塞执行 5 次(约 500ms),期间 say("world") 的 goroutine 有足够时间被调度并交错输出 → 得到混合结果。
- ❌ say("world"); go say("hello"):say("world") 先同步执行完(500ms),此时 main 函数已接近尾声;go say("hello") 虽已启动 goroutine,但 main 立即返回,程序终止 → "hello" 一次都不会打印。
- ❌ go say("world"); go say("hello"):两个 goroutine 均异步启动,但 main 函数在启动后瞬间返回(无任何等待),程序立即退出 → 两个 goroutine 都来不及执行第一条 fmt.Println。
? 关键认知:go 关键字仅发起 goroutine,不保证其执行或完成;它只是将函数加入运行时调度队列,而调度是否发生、何时发生,取决于 main 是否仍在运行。
正确等待 goroutine 完成:使用 sync.WaitGroup
要确保主协程等待所有子 goroutine 执行完毕,必须显式同步。推荐方案是 sync.WaitGroup:
package main
import (
"fmt"
"sync"
"time"
)
func say(wg *sync.WaitGroup, s string) {
defer wg.Done() // 标记当前 goroutine 完成
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
var wg sync.WaitGroup
wg.Add(2) // 声明将等待 2 个 goroutine
go say(&wg, "world")
go say(&wg, "hello")
wg.Wait() // 阻塞直到所有 Done() 被调用
}✅ 输出稳定为 10 行(顺序不定,但 "world" 和 "hello" 各 5 次)。
其他注意事项
- WaitGroup 的 Add() 必须在 go 语句之前调用,否则存在竞态风险(如 Add 还没执行,goroutine 已 Done())。
- 不要传递 WaitGroup 的值拷贝(如 wg),而应传指针 &wg,否则 Done() 操作作用于副本,无法影响主 WaitGroup。
- 替代方案(不推荐初学使用):time.Sleep() 是伪解决方案,不可靠(无法精确预估耗时,且浪费资源);select{} + channel 也可实现同步,但 WaitGroup 更直观、语义清晰。
总之,掌握 goroutine 的生命周期与 main 函数的退出时机,是写出健壮并发 Go 程序的第一课。永远记住:没有等待,就没有保证。


















