Go语言中goroutine无内置父子绑定,需用sync.WaitGroup、context.Context等手动实现协作;main退出会强制终止所有goroutine,故须显式等待或监听取消信号。

goroutine 没有内置父子绑定,必须靠协作机制模拟
Go 语言本身不提供“父 goroutine 自动等待子 goroutine”或“父取消自动终止子”的原生语义。所谓“父子生命周期绑定”,是开发者用 sync.WaitGroup、context.Context 或 channel 手动构造出来的协作契约,不是运行时保障的强制关系。
常见错误现象:只写 go f() 就返回,main 退出后看不到输出;或用了 context.WithCancel 却没在子 goroutine 里监听 ctx.Done(),导致 cancel 调用后子任务仍在跑。
- goroutine 启动即脱离调用栈,无法被“持有”或“追踪”——没有 ID、不能被 kill、不能被 wait
- main goroutine 退出 → 整个进程终止 → 所有其他 goroutine 强制结束(不执行 defer、不释放资源)
- 所谓“绑定”,本质是让父 goroutine 主动阻塞等待,或让子 goroutine 主动响应取消信号
用 sync.WaitGroup 实现“父等子完成”
适用于已知数量、短时可结束的批量任务,比如并发处理一组文件、发起固定数量的 HTTP 请求。
关键点是计数器的增减时机必须严格匹配:
立即学习“go语言免费学习笔记(深入)”;
-
wg.Add(1)必须在go语句之前调用,否则可能 race:子 goroutine 先执行完wg.Done(),而父还没来得及Add -
wg.Done()必须在子 goroutine 函数末尾(或 defer 中),确保无论正常 return 还是 panic 都能触发 -
wg.Wait()放在父 goroutine 中需要同步的位置,比如 main 函数末尾
示例片段:
var wg sync.WaitGroup
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Println("task", id)
}(i)
}
wg.Wait() // 阻塞直到所有 Done 被调用
用 context.WithCancel 实现“父控子取消”
适用于长时运行、需响应外部中断的场景,比如监听 socket、轮询数据库、后台定时任务。
核心陷阱在于:cancel 函数调用后,ctx.Done() 通道才关闭;但子 goroutine 必须主动 select 监听它,否则完全无感知。
- 子 goroutine 内部必须有循环 +
select,且至少一个分支是<-ctx.Done() - 不要在子 goroutine 中直接调用
cancel()(除非是自触发逻辑),否则会提前终结整个 context 树 - 若子任务还需启动孙 goroutine,应把同一
ctx传下去,而不是新建context.Background()
典型结构:
ctx, cancel := context.WithCancel(context.Background())
defer cancel() // 父 goroutine 结束前清理
<p>go func(ctx context.Context) {
for {
select {
case <-ctx.Done():
fmt.Println("received cancel, exiting")
return
default:
// do work
time.Sleep(500 * time.Millisecond)
}
}
}(ctx)
为什么不能混用 WaitGroup 和 Context 做同一目标
比如既想等子 goroutine 完成,又想支持超时取消——这时容易写出“看似正确实则失效”的代码。
常见翻车点:
- 在
wg.Wait()前加time.AfterFunc调用cancel(),但子 goroutine 没监听 ctx,cancel 就只是个空操作 - 用
context.WithTimeout包裹wg.Wait(),但wg.Wait()本身不接受 ctx,无法响应超时 - 子 goroutine 里同时做
wg.Done()和监听ctx.Done(),但未保证两者互斥,可能 Done 多次或漏调
真正可靠的组合是:context.WithTimeout 控制单个子任务最大执行时间,WaitGroup 控制“有多少个子任务要等”,两者职责分离。父 goroutine 的等待逻辑应写成:
done := make(chan struct{})
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
// all done
case <-ctx.Done():
// timeout or cancelled
}
真正的难点不在语法,而在判断哪个机制该用在哪一层:WaitGroup 适合编排层(我启了多少个),Context 适合控制层(它们能不能继续跑)。漏掉任一环节,就等于放任 goroutine 在后台裸奔。


















