讲解
goroutine 是 Go 的轻量级线程:在函数调用前加一个 go 关键字,这个函数就在新的 goroutine 里并发执行,主流程不等它、继续往下走。goroutine 由 Go 运行时调度,初始栈只有几 KB,一个程序轻松起几万个——相比之下操作系统线程动辄 MB 级栈、上千个就吃力。「并发是 Go 的一等公民」,说的就是 goroutine 的廉价与易用。
主 goroutine(main 函数)退出时,所有其他 goroutine 会被直接「杀死」,不管干完没有。所以启动 goroutine 后必须有等待机制,最常用的就是 sync.WaitGroup:Add(1) 登记一个任务,任务完成时 Done(),主流程 Wait() 阻塞到计数归零。约定是 Add 在启动 goroutine 之前调用,Done 用 defer 保证必达。
多个 goroutine 读写同一份数据会产生「数据竞争」——结果不可预期,而且平时可能完全看不出来。纪律只有一条:要么不共享(各自操作各自的数据),要么共享时用 channel 或锁来保护。go run -race 或 go test -race 开启竞争检测器,能在开发和测试阶段把竞争抓出来,是并发代码的必做检查。
示例
启动 goroutine 并用 WaitGroup 等待全部完成:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done() // 完成时登记,defer 保证必达
fmt.Printf("工人 %d 开始干活\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 3; i++ {
wg.Add(1) // 启动前登记
go worker(i, &wg)
}
wg.Wait() // 阻塞直到所有工人完工
fmt.Println("全部完成")
}
并发计算:每个 goroutine 写自己的下标,互不共享即无竞争:
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
results := make([]int, 5)
for i := 0; i < 5; i++ {
wg.Add(1)
go func() {
defer wg.Done()
results[i] = i * i // Go 1.22 起循环变量每次迭代独立
}()
}
wg.Wait()
fmt.Println("平方结果:", results)
}
常见坑
- main 提前返回,goroutine 被秒杀:起了 goroutine 就结束 main,子任务可能一行都没执行。用 WaitGroup 或 channel 同步,不要靠 time.Sleep 赌时间。
- WaitGroup 计数失衡:Add 的次数和 Done 的次数不一致,要么 Wait 永不返回(死锁),要么提前放行。defer wg.Done() 是最稳的配对方式。
- 并发写同一变量:两个 goroutine 同时 counter++ 不是原子操作,结果会少加。用 -race 跑测试能抓出来;修法是 channel、sync.Mutex 或 atomic。
- 把 WaitGroup 按值拷贝:wg 必须传指针(或放结构体里传指针),值拷贝会让 Done 减的不是同一个计数器。go vet 会检查这类拷贝。
- 以为 goroutine 数量无上限:goroutine 虽廉价但不是免费,每个都吃内存,失控创建(比如对每个请求无限制地起)照样撑爆内存。高并发场景用固定数量的 worker 池。
小结
go 关键字起并发,WaitGroup 等待收工,共享数据要同步,-race 抓竞争。下一节学习 goroutine 之间通信的正道:channel。