漏掉括号导致goroutine未启动是最常见语法陷阱:go func(){...}不执行,必须写成go func(){...}();编译器不报错但逻辑静默失效,尤其在封装“栅栏”函数时易误判已调度。

goroutine 启动时漏掉 () 导致任务根本没跑
这是最常被忽略的语法陷阱:写 go func() { ... } 不会启动协程,必须加括号变成 go func() { ... }() 才真正执行。编译器不报错,但逻辑静默失效——尤其在封装成“栅栏”函数时,容易误以为已调度。
典型错误场景是把并发控制逻辑抽成函数,比如:
func startBarrier() {
go func() { /* 阶段一 */ }
go func() { /* 阶段二 */ }
}这两行什么都不会发生。正确写法是:
go func() { /* 阶段一 */ }()- 或先赋值再调用:
f := func() { ... }; go f()
用 sync.WaitGroup 控制多阶段依赖顺序
匿名函数封装的栅栏,本质是把多个阶段的启动和等待逻辑收口到一处。直接靠 channel 串联阶段容易阻塞死锁,而 WaitGroup 更适合表达“等全部阶段完成”这一意图。
立即学习“go语言免费学习笔记(深入)”;
关键点:
- 每个阶段启动前调用
wg.Add(1),结束时调用wg.Done() - 主 goroutine 在所有
go启动后立即wg.Wait(),而非轮询或超时等待 - 避免在匿名函数里重复
defer wg.Done()—— 它只应在该阶段函数末尾调用一次
示例中阶段二依赖阶段一结果,可把结果通过闭包变量传递,而不是靠 channel 同步(除非真需要数据流):
var result int
wg.Add(1)
go func() {
defer wg.Done()
result = stageOne()
}()
wg.Wait() // 等阶段一完成
wg.Add(1)
go func() {
defer wg.Done()
stageTwo(result) // 安全使用 result
}()闭包捕获循环变量引发的阶段错乱
当用 for 循环批量启动多个阶段栅栏时,若直接闭包外层循环变量,所有阶段会共享最终值。比如:
for i := 0; i < 3; i++ {
go func() {
fmt.Println("stage", i) // 全部输出 3
}()
}修复方式只有两种可靠路径:
- Go 1.22+:直接写
for i := range stages { go func(i int) { ... }(i) },利用新版循环变量作用域语义 - 兼容老版本:显式绑定副本,
for i := 0; i
注意:仅靠 fmt.Printf("stage %d", i) 这种读操作无法暴露问题,一旦阶段内有写操作(如修改 map、发请求带 id),就会出现竞态或错发。
栅栏函数里混用 context.Context 和 WaitGroup 的边界
单纯等完成用 WaitGroup 足够;但若任一阶段可能超时、需取消下游、或要响应 os.Signal,就必须引入 context.Context。
常见误用:
- 在匿名函数里只传
ctx却没检查ctx.Err(),导致取消信号被忽略 - 对每个阶段单独创建子 context(如
ctx, cancel := context.WithTimeout(parent, t)),但没在阶段结束时调用cancel(),造成 context 泄漏 - 把
WaitGroup和context.WithCancel混用却不协调:比如wg.Wait()返回后才调cancel(),但某些阶段可能已提前退出并释放资源
推荐组合模式:主函数用 WaitGroup 等所有阶段 goroutine 结束,每个阶段内部用 select { case 响应取消,且阶段结束时统一由 defer 触发 cleanup。
真正容易被忽略的不是怎么写语法,而是哪个阶段该持有 context、哪个该响应 cancel、以及 wg.Done() 和 cancel() 的调用顺序是否构成资源释放的确定性链路——它不体现在代码行数里,而藏在 goroutine 的实际退出时机中。


















