goroutine本质是加go前缀启动函数,但需同步控制、显式传参、配合channel或WaitGroup;应避免匿名函数立即执行,改用命名函数调用,并警惕循环中闭包捕获变量陷阱。

goroutine 本质就是加个 go 前缀启动函数,但不加同步控制就跑飞、不传参就闭包踩坑、不配 channel 或 sync.WaitGroup 就收不到结果——它极简,也极容易“看似在跑,其实没跑完”。
怎么启动 goroutine:别写 go func() { ... }(),直接调函数
新手常把 goroutine 启动写成匿名函数立即执行的形式,比如:
go func() { fmt.Println("hello") }()
这没错,但多数时候是画蛇添足。真正该做的是:把逻辑封装成普通函数,用 go 调用它。
- 更清晰:函数名自带语义,便于测试和复用
- 更安全:避免闭包捕获循环变量(见下节)
- 更可控:参数显式传递,栈隔离明确
✅ 正确姿势:
立即学习“go语言免费学习笔记(深入)”;
func say(msg string) { fmt.Println(msg) } func main() { go say("hello from goroutine") time.Sleep(10 * time.Millisecond) // 临时保命,实际要用 wait 或 channel }
for 循环里启多个 goroutine:闭包陷阱必须破
下面这段代码几乎 100% 输出全是 3:
for i := 0; i < 3; i++ { go func() { fmt.Println(i) // 全部打印 3! }() }
原因:所有 goroutine 共享同一个变量 i 的地址,等它们真正执行时,for 早已结束,i == 3。
- ❌ 错误解法:不传参、不复制、只靠
time.Sleep拖延 - ✅ 解法一:把
i当参数传进去(推荐)
for i := 0; i < 3; i++ { go func(val int) { fmt.Println(val) // 输出 0, 1, 2 }(i) }
- ✅ 解法二:在循环内重新声明变量(效果等价)
for i := 0; i < 3; i++ { i := i // 新建局部变量 go func() { fmt.Println(i) // 输出 0, 1, 2 }() }
怎么等 goroutine 结束:别靠 time.Sleep,用 sync.WaitGroup 或 channel
time.Sleep 是调试玩具,生产环境绝对不能用——它既不准(可能等不够或等太久),也不可靠(CPU 负载高时调度延迟变大)。
- ✅ 场景一:只关心“全部完成”,不关心返回值 → 用
sync.WaitGroup
func worker(id int, wg *sync.WaitGroup) { defer wg.Done() fmt.Printf("worker %d donen", id) } func main() { var wg sync.WaitGroup for i := 1; i <= 3; i++ { wg.Add(1) go worker(i, &wg) } wg.Wait() // 阻塞直到所有 Done() }
- ✅ 场景二:要拿结果、要控制流、要解耦 → 用
chan
c := make(chan int, 3) go func() { c <- 42 }() go func() { c <- 100 -1>
goroutine 和 channel 组合:别漏 close,别让 range 卡死
用 for range ch 接收 channel 数据时,如果没人关 channel,接收方会永远阻塞在 range 上——哪怕所有发送 goroutine 已退出。
- ❌ 错误:只发不关
ch := make(chan int) go func() { ch <- 1; ch <- 2 }()>
- ✅ 正确:发送方完成前调用
close(ch),且确保只关一次
ch := make(chan int) go func() { ch <- 1 ch <- 2 close(ch)>
真正难的不是语法,而是谁负责关闭、什么时候关、并发关闭是否重复——这些得结合业务生命周期设计,不是加一行 close 就万事大吉。