Go 程序中启动 Goroutine 后若未合理同步,主函数可能提前退出,导致并发任务被静默丢弃;单次 time.Sleep 不可靠,应使用通道(channel)或 sync.WaitGroup 实现确定性等待。
go 程序中启动 goroutine 后若未合理同步,主函数可能提前退出,导致并发任务被静默丢弃;单次 `time.sleep` 不可靠,应使用通道(channel)或 `sync.waitgroup` 实现确定性等待。
在 Go 中,go 关键字用于异步启动一个 Goroutine,但它不会阻塞主协程。这意味着 main() 函数在启动所有 Goroutine 后会立即继续执行,并在到达末尾时退出整个程序——此时尚未完成的 Goroutine 会被强制终止,输出丢失。这正是你观察到“无输出”或“需两次 time.Sleep”的根本原因:不是语法要求两次休眠,而是单次 1000ms 仍不足以保证所有 Goroutine 完成打印(尤其在高负载或调度延迟下),而两次叠加只是偶然凑够了时间。这种基于固定休眠的方案本质是竞态且不可靠的。
✅ 正确做法:用同步机制等待 Goroutine 完成
方案一:使用 sync.WaitGroup(推荐,语义清晰)
package main
import (
"fmt"
"sync"
)
func doSomething(integer int, wg *sync.WaitGroup) {
defer wg.Done() // 通知 WaitGroup 当前任务完成
fmt.Println(integer)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 10; i++ {
wg.Add(1) // 注册一个待等待的任务
go doSomething(i, &wg)
}
wg.Wait() // 阻塞直到所有注册任务调用 Done()
}✅ 优势:明确表达“等待 N 个任务完成”,线程安全,无需关心执行时长。
方案二:使用通道(Channel)协调
package main
import "fmt"
func doSomething(integer int, done chan<- bool) {
fmt.Println(integer)
done <- true // 发送完成信号
}
func main() {
done := make(chan bool, 10) // 缓冲通道,避免 Goroutine 阻塞
for i := 1; i <= 10; i++ {
go doSomething(i, done)
}
// 等待全部 10 个信号
for i := 0; i < 10; i++ {
<-done
}
}✅ 优势:天然支持跨 Goroutine 通信与信号传递;适合需传递结果或状态的场景。
⚠️ 注意事项与最佳实践
- 避免 time.Sleep 做同步:它无法保证逻辑正确性(如 Sleep(2000) 在慢机器上仍可能失败),且使程序响应变差。
- 注意变量捕获陷阱:原代码中 i 是循环变量,所有 Goroutine 共享同一地址,可能导致打印重复或错误值(如全输出 11)。应传参 i(如 go doSomething(i)),而非引用循环变量。
- 资源清理:若 Goroutine 涉及 I/O 或内存分配,确保超时控制或取消机制(如 context.Context)。
- 学习延伸:Go Tour 的 Concurrency 章节 提供了更贴近实际的生产级示例,建议动手实践。
总之,Goroutine 的轻量不等于“无需管理”——主动同步才是并发安全的基石。选择 WaitGroup 或通道,让等待变得可预测、可验证、可维护。

















